View Javadoc
1   /*
2    * This file is part of ***  M y C o R e  ***
3    * See http://www.mycore.de/ for details.
4    *
5    * MyCoRe is free software: you can redistribute it and/or modify
6    * it under the terms of the GNU General Public License as published by
7    * the Free Software Foundation, either version 3 of the License, or
8    * (at your option) any later version.
9    *
10   * MyCoRe is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with MyCoRe.  If not, see <http://www.gnu.org/licenses/>.
17   */
18  
19  package org.mycore.frontend.cli;
20  
21  import java.io.File;
22  import java.io.FileOutputStream;
23  import java.io.IOException;
24  import java.io.OutputStream;
25  import java.io.UncheckedIOException;
26  import java.text.MessageFormat;
27  import java.text.SimpleDateFormat;
28  import java.util.ArrayList;
29  import java.util.Arrays;
30  import java.util.Collections;
31  import java.util.HashMap;
32  import java.util.List;
33  import java.util.Locale;
34  import java.util.Map;
35  import java.util.Map.Entry;
36  import java.util.Objects;
37  import java.util.Optional;
38  import java.util.concurrent.atomic.AtomicInteger;
39  import java.util.function.Function;
40  import java.util.function.Predicate;
41  import java.util.stream.Collectors;
42  import java.util.stream.Stream;
43  
44  import javax.xml.parsers.ParserConfigurationException;
45  import javax.xml.transform.OutputKeys;
46  import javax.xml.transform.Source;
47  import javax.xml.transform.Transformer;
48  import javax.xml.transform.TransformerException;
49  import javax.xml.transform.TransformerFactory;
50  import javax.xml.transform.sax.SAXSource;
51  import javax.xml.transform.stream.StreamResult;
52  import javax.xml.transform.stream.StreamSource;
53  
54  import org.apache.commons.lang3.function.FailableBiConsumer;
55  import org.apache.logging.log4j.LogManager;
56  import org.apache.logging.log4j.Logger;
57  import org.jdom2.Document;
58  import org.jdom2.JDOMException;
59  import org.jdom2.filter.Filters;
60  import org.jdom2.transform.JDOMResult;
61  import org.jdom2.transform.JDOMSource;
62  import org.jdom2.xpath.XPathExpression;
63  import org.jdom2.xpath.XPathFactory;
64  import org.mycore.access.MCRAccessException;
65  import org.mycore.backend.jpa.MCREntityManagerProvider;
66  import org.mycore.common.MCRConstants;
67  import org.mycore.common.MCRException;
68  import org.mycore.common.MCRPersistenceException;
69  import org.mycore.common.MCRSessionMgr;
70  import org.mycore.common.MCRStreamUtils;
71  import org.mycore.common.config.MCRConfiguration2;
72  import org.mycore.common.content.MCRBaseContent;
73  import org.mycore.common.content.MCRContent;
74  import org.mycore.common.content.MCRJDOMContent;
75  import org.mycore.common.content.MCRSourceContent;
76  import org.mycore.common.content.transformer.MCRContentTransformer;
77  import org.mycore.common.content.transformer.MCRContentTransformerFactory;
78  import org.mycore.common.xml.MCREntityResolver;
79  import org.mycore.common.xml.MCRLayoutTransformerFactory;
80  import org.mycore.common.xml.MCRURIResolver;
81  import org.mycore.common.xml.MCRXMLHelper;
82  import org.mycore.common.xml.MCRXMLParserFactory;
83  import org.mycore.common.xsl.MCRErrorListener;
84  import org.mycore.datamodel.common.MCRAbstractMetadataVersion;
85  import org.mycore.datamodel.common.MCRActiveLinkException;
86  import org.mycore.datamodel.common.MCRLinkTableManager;
87  import org.mycore.datamodel.common.MCRXMLMetadataManager;
88  import org.mycore.datamodel.metadata.MCRBase;
89  import org.mycore.datamodel.metadata.MCRDerivate;
90  import org.mycore.datamodel.metadata.MCRMetaEnrichedLinkID;
91  import org.mycore.datamodel.metadata.MCRMetaLinkID;
92  import org.mycore.datamodel.metadata.MCRMetadataManager;
93  import org.mycore.datamodel.metadata.MCRObject;
94  import org.mycore.datamodel.metadata.MCRObjectID;
95  import org.mycore.datamodel.metadata.MCRObjectUtils;
96  import org.mycore.frontend.cli.annotation.MCRCommand;
97  import org.mycore.frontend.cli.annotation.MCRCommandGroup;
98  import org.mycore.tools.MCRTopologicalSort;
99  import org.xml.sax.SAXException;
100 import org.xml.sax.SAXParseException;
101 import org.xml.sax.XMLReader;
102 
103 import jakarta.persistence.EntityManager;
104 import jakarta.persistence.TypedQuery;
105 
106 /**
107  * Provides static methods that implement commands for the MyCoRe command line interface. Robert: Ideas for clean-up -
108  * "transform ..." and "xslt..." do the same thing and should thereform be named uniquely - "transformm ...." -
109  * "delete by Query ..." can be deleted - "select ..." and "delete selected ..." supply the same behaviour in 2 commands
110  * - "list objects matching ..." can be deleted - "select ..." and "list selected" supply the same behaviour in 2
111  * commands
112  *
113  * @author Jens Kupferschmidt
114  * @author Frank Lützenkirchen
115  * @author Robert Stephan
116  * @version $Revision$ $Date$
117  */
118 @MCRCommandGroup(name = "Object Commands")
119 public class MCRObjectCommands extends MCRAbstractCommands {
120 
121     private static final String EXPORT_OBJECT_TO_DIRECTORY_WITH_STYLESHEET_COMMAND
122         = "export object {0} to directory {1} with stylesheet {2}";
123 
124     /** The logger */
125     private static final Logger LOGGER = LogManager.getLogger(MCRObjectCommands.class);
126 
127     /** Default transformer script */
128     public static final String DEFAULT_STYLE = "save-object.xsl";
129 
130     /** Static compiled transformer stylesheets */
131     private static final Map<String, Transformer> TRANSFORMER_CACHE = new HashMap<>();
132 
133     public static void setSelectedObjectIDs(List<String> selected) {
134         LOGGER.info("{} objects selected", selected.size());
135         MCRSessionMgr.getCurrentSession().put("mcrSelectedObjects", selected);
136     }
137 
138     @SuppressWarnings("unchecked")
139     public static List<String> getSelectedObjectIDs() {
140         final List<String> list = (List<String>) MCRSessionMgr.getCurrentSession().get("mcrSelectedObjects");
141         if (list == null) {
142             return Collections.EMPTY_LIST;
143         }
144         return list;
145     }
146 
147     @MCRCommand(
148         syntax = "select objects with xpath {0}",
149         help = "Selects MCRObjects with XPath {0}, if that XPath evaluates to a non-empty result list" +
150             " (this command may take a while, use with care in case of a large number of objects)",
151         order = 10)
152     public static void selectObjectsWithXpath(String xPath) throws Exception {
153 
154         XPathExpression<Object> xPathExpression = XPathFactory
155             .instance()
156             .compile(xPath, Filters.fpassthrough(), null, MCRConstants.getStandardNamespaces());
157 
158         List<String> selectedObjectIds = MCRXMLMetadataManager
159             .instance()
160             .listIDs()
161             .stream()
162             .filter(id -> !id.contains("_derivate_"))
163             .map(MCRObjectID::getInstance)
164             .map(MCRMetadataManager::retrieveMCRObject)
165             .filter(mcrObject -> !xPathExpression.evaluate(mcrObject.createXML()).isEmpty())
166             .map(MCRObject::getId)
167             .map(MCRObjectID::toString)
168             .collect(Collectors.toList());
169 
170         MCRObjectCommands.setSelectedObjectIDs(selectedObjectIds);
171 
172     }
173 
174     @MCRCommand(
175         syntax = "select descendants of object {0}",
176         help = "Selects MCRObjects that are descendants of {0} (children, grandchildren, ...) and {0} itself.",
177         order = 15)
178     public static void selectDescendantObjects(String id) throws Exception {
179         List<String> descendants = new ArrayList<String>();
180         if (MCRMetadataManager.exists(MCRObjectID.getInstance(id))) {
181             fillWithDescendants(id, descendants);
182         }
183         MCRObjectCommands.setSelectedObjectIDs(descendants);
184     }
185 
186     private static void fillWithDescendants(String mcrObjID, List<String> descendants) {
187         descendants.add(mcrObjID);
188         //add child objects
189         for (String childID : MCRLinkTableManager.instance().getSourceOf(mcrObjID,
190             MCRLinkTableManager.ENTRY_TYPE_PARENT)) {
191             descendants.add(childID);
192             fillWithDescendants(childID, descendants);
193         }
194     }
195 
196     /**
197      * Delete all MCRObject from the datastore for a given type.
198      *
199      * @param type
200      *            the type of the MCRObjects that should be deleted
201      */
202     @MCRCommand(
203         syntax = "delete all objects of type {0}",
204         help = "Removes MCRObjects of type {0}.",
205         order = 20)
206     public static List<String> deleteAllObjects(String type) {
207         return MCRCommandUtils.getIdsForType(type)
208             .map(id -> "delete object " + id)
209             .collect(Collectors.toList());
210     }
211 
212     /**
213      * Delete all MCRObjects from the datastore in topological order
214      *
215      */
216     @MCRCommand(
217         syntax = "delete all objects in topological order",
218         help = "Removes all MCRObjects in topological order.",
219         order = 25)
220     public static List<String> deleteTopologicalAllObjects() {
221         final List<String> objectIds = MCRXMLMetadataManager.instance().listIDs();
222         String[] objects = objectIds.stream().filter(id -> !id.contains("_derivate_")).toArray(String[]::new);
223         MCRTopologicalSort<String> ts = new MCRTopologicalSort<>();
224         MCRTopologicalSort.prepareMCRObjects(ts, objects);
225         int[] order = ts.doTopoSort();
226 
227         List<String> cmds = new ArrayList<>(objectIds.size());
228         if (order != null) {
229             //delete in reverse order
230             for (int o = order.length - 1; o >= 0; o--) {
231                 cmds.add("delete object " + ts.getNodeName(order[o]));
232             }
233         }
234         return cmds;
235     }
236 
237     @MCRCommand(
238         syntax = "check for circles in topological order",
239         help = "Checks if there are circular dependencies in the parent child relationships of MCRObjects.",
240         order = 25)
241     public static void checkForCircles() {
242         final List<String> objectIds = MCRXMLMetadataManager.instance().listIDs();
243         String[] objects = objectIds.stream().filter(id -> !id.contains("_derivate_")).toArray(String[]::new);
244         MCRTopologicalSort<String> ts = new MCRTopologicalSort<>();
245         MCRTopologicalSort.prepareMCRObjects(ts, objects);
246         int[] order = ts.doTopoSort();
247         if (order != null) {
248             LOGGER.info("OK - No circles detected!");
249         }
250     }
251 
252     /**
253      * Delete a MCRObject from the datastore.
254      *
255      * @param id
256      *            the id of the MCRObject that should be deleted
257      * @throws MCRPersistenceException  if a persistence problem is occurred
258      * @throws MCRAccessException see {@link MCRMetadataManager#deleteMCRObject(MCRObjectID)}
259      * @throws MCRActiveLinkException if object is referenced by other objects
260      */
261     @MCRCommand(
262         syntax = "delete object {0}",
263         help = "Removes a MCRObject with the MCRObjectID {0}",
264         order = 40)
265     public static void delete(String id) throws MCRPersistenceException, MCRActiveLinkException, MCRAccessException {
266         MCRObjectID mcrId = MCRObjectID.getInstance(id);
267         MCRMetadataManager.deleteMCRObject(mcrId);
268         LOGGER.info("{} deleted.", mcrId);
269     }
270 
271     /**
272      * Runs though all mycore objects which are linked with the given object and removes its link. This includes
273      * parent/child relations and all {@link MCRMetaLinkID} in the metadata section.
274      *
275      * @param id
276      *            the id of the MCRObject that should be deleted
277      * @throws MCRPersistenceException  if a persistence problem is occurred
278      */
279     @MCRCommand(
280         syntax = "clear links of object {0}",
281         help = "removes all links of this object, including parent/child relations"
282             + " and all MetaLinkID's in the metadata section",
283         order = 45)
284     public static void clearLinks(String id) throws MCRPersistenceException {
285         final MCRObjectID mcrId = MCRObjectID.getInstance(id);
286         AtomicInteger counter = new AtomicInteger(0);
287         MCRObjectUtils.removeLinks(mcrId).forEach(linkedObject -> {
288             try {
289                 LOGGER.info("removing link '{}' of '{}'.", mcrId, linkedObject.getId());
290                 MCRMetadataManager.update(linkedObject);
291                 counter.incrementAndGet();
292             } catch (Exception exc) {
293                 LOGGER.error(String.format(Locale.ROOT, "Unable to update object '%s'", linkedObject), exc);
294             }
295         });
296         LOGGER.info("{} link(s) removed of {}.", counter.get(), mcrId);
297     }
298 
299     /**
300      * Delete MCRObject's form ID to ID from the datastore.
301      *
302      * @param idFrom
303      *            the start ID for deleting the MCRObjects
304      * @param idTo
305      *            the stop ID for deleting the MCRObjects
306      * @return list of delete commands
307      */
308     @MCRCommand(
309         syntax = "delete object from {0} to {1}",
310         help = "Removes MCRObjects in the number range between the MCRObjectID {0} and {1}.",
311         order = 30)
312     public static List<String> deleteFromTo(String idFrom, String idTo) {
313         return MCRCommandUtils.getIdsFromIdToId(idFrom, idTo)
314             .map(id -> "delete object " + id)
315             .collect(Collectors.toList());
316     }
317 
318     /**
319      * Load MCRObject's from all XML files in a directory in proper order (respecting parent-child-relationships).
320      *
321      * @param directory
322      *            the directory containing the XML files
323      */
324     @MCRCommand(
325         syntax = "load all objects in topological order from directory {0}",
326         help = "Loads all MCRObjects form the directory {0} to the system "
327             + "respecting the order of parents and children.",
328         order = 75)
329     public static List<String> loadTopologicalFromDirectory(String directory) {
330         return processFromDirectory(true, directory, false);
331     }
332 
333     /**
334      * Update MCRObject's from all XML files in a directory in proper order (respecting parent-child-relationships).
335      *
336      * @param directory
337      *            the directory containing the XML files
338      */
339     @MCRCommand(
340         syntax = "update all objects in topological order from directory {0}",
341         help = "Updates all MCRObjects from the directory {0} in the system "
342             + "respecting the order of parents and children.",
343         order = 95)
344     public static List<String> updateTopologicalFromDirectory(String directory) {
345         return processFromDirectory(true, directory, true);
346     }
347 
348     /**
349      * Load MCRObject's from all XML files in a directory.
350      *
351      * @param directory
352      *            the directory containing the XML files
353      */
354     @MCRCommand(
355         syntax = "load all objects from directory {0}",
356         help = "Loads all MCRObjects from the directory {0} to the system. " +
357             "If the numerical part of a provided ID is zero, a new ID with the same project ID and type is assigned.",
358         order = 70)
359     public static List<String> loadFromDirectory(String directory) {
360         return processFromDirectory(false, directory, false);
361     }
362 
363     /**
364      * Update MCRObject's from all XML files in a directory.
365      *
366      * @param directory
367      *            the directory containing the XML files
368      */
369     @MCRCommand(
370         syntax = "update all objects from directory {0}",
371         help = "Updates all MCRObjects from the directory {0} in the system.",
372         order = 90)
373     public static List<String> updateFromDirectory(String directory) {
374         return processFromDirectory(false, directory, true);
375     }
376 
377     /**
378      * Load or update MCRObject's from all XML files in a directory.
379      *
380      * @param topological
381      *            if true, the dependencies of parent and child objects will be respected
382      * @param directory
383      *            the directory containing the XML files
384      * @param update
385      *            if true, object will be updated, else object is created
386      */
387     private static List<String> processFromDirectory(boolean topological, String directory, boolean update) {
388         File dir = new File(directory);
389 
390         if (!dir.isDirectory()) {
391             LOGGER.warn("{} ignored, is not a directory.", directory);
392             return null;
393         }
394 
395         String[] list = dir.list();
396         if (list == null || list.length == 0) {
397             LOGGER.warn("No files found in directory {}", directory);
398             return null;
399         }
400 
401         Predicate<String> isMetaXML = file -> file.endsWith(".xml") && !file.contains("derivate");
402         Function<String, String> cmdFromFile = file -> (update ? "update" : "load") + " object from file "
403             + new File(dir, file).getAbsolutePath();
404         if (topological) {
405             MCRTopologicalSort<String> ts = new MCRTopologicalSort<>();
406             MCRTopologicalSort.prepareData(ts, list, dir.toPath());
407             return Optional.ofNullable(ts.doTopoSort())
408                 .map(Arrays::stream)
409                 .map(is -> is.mapToObj(i -> list[i]))
410                 .orElse(Stream.empty())
411                 .filter(isMetaXML)
412                 .map(cmdFromFile)
413                 .collect(Collectors.toList());
414         } else {
415             return Arrays.stream(list)
416                 .filter(isMetaXML)
417                 .sorted()
418                 .map(cmdFromFile)
419                 .collect(Collectors.toList());
420         }
421     }
422 
423     /**
424      * Load a MCRObjects from an XML file.
425      *
426      * @param file
427      *            the location of the xml file
428      * @throws MCRAccessException see {@link MCRMetadataManager#create(MCRObject)}
429      */
430     @MCRCommand(
431         syntax = "load object from file {0}",
432         help = "Loads an MCRObject from the file {0} to the system. " +
433             "If the numerical part of the provided ID is zero, a new ID with the same project ID and type is assigned.",
434         order = 60)
435     public static boolean loadFromFile(String file) throws MCRException, SAXParseException,
436         IOException, MCRAccessException {
437         return loadFromFile(file, true);
438     }
439 
440     /**
441      * Load a MCRObjects from an XML file.
442      *
443      * @param file
444      *            the location of the xml file
445      * @param importMode
446      *            if true, servdates are taken from xml file
447      * @throws MCRAccessException see {@link MCRMetadataManager#update(MCRObject)}
448      */
449     public static boolean loadFromFile(String file, boolean importMode) throws MCRException,
450         SAXParseException, IOException, MCRAccessException {
451         return processFromFile(new File(file), false, importMode);
452     }
453 
454     /**
455      * Update a MCRObject's from an XML file.
456      *
457      * @param file
458      *            the location of the xml file
459      * @throws MCRAccessException see {@link MCRMetadataManager#update(MCRObject)}
460      */
461     @MCRCommand(
462         syntax = "update object from file {0}",
463         help = "Updates a MCRObject from the file {0} in the system.",
464         order = 80)
465     public static boolean updateFromFile(String file) throws MCRException, SAXParseException,
466         IOException, MCRAccessException {
467         return updateFromFile(file, true);
468     }
469 
470     /**
471      * Update a MCRObject's from an XML file.
472      *
473      * @param file
474      *            the location of the xml file
475      * @param importMode
476      *            if true, servdates are taken from xml file
477      * @throws MCRAccessException see {@link MCRMetadataManager#update(MCRObject)}
478      */
479     public static boolean updateFromFile(String file, boolean importMode) throws MCRException,
480         SAXParseException, IOException, MCRAccessException {
481         return processFromFile(new File(file), true, importMode);
482     }
483 
484     /**
485      * Load or update an MCRObject from an XML file. If the numerical part of the contained ID is zero,
486      * a new ID with the same project ID and type is assigned.
487      *
488      * @param file
489      *            the location of the xml file
490      * @param update
491      *            if true, object will be updated, else object is created
492      * @param importMode
493      *            if true, servdates are taken from xml file
494      * @throws SAXParseException
495      *            unable to build the mycore object from the file's URI
496      * @throws MCRException
497      *            the parent of the given object does not exists
498      * @throws MCRAccessException
499      *            if write permission is missing
500      */
501     private static boolean processFromFile(File file, boolean update, boolean importMode)
502         throws MCRException, SAXParseException, IOException, MCRAccessException {
503         if (!file.getName().endsWith(".xml")) {
504             LOGGER.warn("{} ignored, does not end with *.xml", file);
505             return false;
506         }
507 
508         if (!file.isFile()) {
509             LOGGER.warn("{} ignored, is not a file.", file);
510             return false;
511         }
512 
513         LOGGER.info("Reading file {} ...", file);
514 
515         MCRObject mcrObject = new MCRObject(file.toURI());
516         if (mcrObject.hasParent()) {
517             MCRObjectID parentID = mcrObject.getStructure().getParentID();
518             if (!MCRMetadataManager.exists(mcrObject.getStructure().getParentID())) {
519                 throw new MCRException("The parent object " + parentID + "does not exist for " + mcrObject + ".");
520             }
521         }
522         mcrObject.setImportMode(importMode);
523         LOGGER.debug("Label --> {}", mcrObject.getLabel());
524 
525         if (update) {
526             MCRMetadataManager.update(mcrObject);
527             LOGGER.info("{} updated.", mcrObject.getId());
528         } else {
529             MCRMetadataManager.create(mcrObject);
530             LOGGER.info("{} loaded.", mcrObject.getId());
531         }
532 
533         return true;
534     }
535 
536     /**
537      * Shows the next free MCRObjectIDs.
538      *
539      * @param base
540      *            the base String of the MCRObjectID
541      */
542     public static void showNextID(String base) {
543 
544         try {
545             LOGGER.info("The next free ID  is {}", MCRObjectID.getNextFreeId(base));
546         } catch (MCRException ex) {
547             LOGGER.error(ex.getMessage());
548         }
549     }
550 
551     /**
552      * Shows the last used MCRObjectIDs.
553      *
554      * @param base
555      *            the base String of the MCRObjectID
556      */
557     public static void showLastID(String base) {
558         try {
559             LOGGER.info("The last used ID  is {}", MCRObjectID.getLastID(base));
560         } catch (MCRException ex) {
561             LOGGER.error(ex.getMessage());
562         }
563     }
564 
565     /**
566      * Export an MCRObject to a file named <em>MCRObjectID</em>.xml in a directory named <em>dirname</em>.
567      * The method uses the converter stylesheet <em>style</em>.xsl.
568      *
569      * @param id
570      *            the id of the MCRObject to be save.
571      * @param dirname
572      *            the dirname to store the object
573      * @param style
574      *            the type of the stylesheet
575      */
576     @MCRCommand(
577         syntax = EXPORT_OBJECT_TO_DIRECTORY_WITH_STYLESHEET_COMMAND,
578         help = "Stores the MCRObject with the MCRObjectID {0} to the directory {1} with the stylesheet {2}-object.xsl."
579             + " For {2}, the default is xsl/save.",
580         order = 110)
581     public static void exportWithStylesheet(String id, String dirname, String style) {
582         exportWithStylesheet(id, id, dirname, style);
583     }
584 
585     /**
586      * Export an MCRObject to a file named <em>MCRObjectID</em>.xml in a directory named <em>dirname</em>.
587      * The method use the content transformer <em>transname</em>xsl.
588      *
589      * @param id
590      *            the id of the MCRObject to be save.
591      * @param dirname
592      *            the dirname to store the object
593      * @param transname
594      *            the name of the transformer
595      */
596     @MCRCommand(
597         syntax = "export object {0} to directory {1} with transformer {2}",
598         help = "Stores the MCRObject with the MCRObjectID {0} to the directory {1} with the transformer {2}.",
599         order = 110)
600     public static void exportWithTransformer(String id, String dirname, String transname) {
601         exportWithTransformer(id, id, dirname, transname);
602     }
603 
604     /**
605      * Export any MCRObject's to files named <em>MCRObjectID</em>.xml in a directory named <em>dirname</em>.
606      * Exporting starts with <em>fromID</em> and ends with <em>toID</em>. IDs that aren't found will be skipped.
607      * The method uses the converter stylesheet <em>style</em>.xsl.
608      *
609      * @param fromID
610      *            the ID of the MCRObject from be save.
611      * @param toID
612      *            the ID of the MCRObject to be save.
613      * @param dirname
614      *            the filename to store the object
615      * @param style
616      *            the type of the stylesheet
617      */
618     @MCRCommand(
619         syntax = "export objects from {0} to {1} to directory {2} with stylesheet {3}",
620         help = "Stores all MCRObjects with MCRObjectID's between {0} and {1} to the directory {2} "
621             + "with the stylesheet {3}-object.xsl. For {3}, the default is xsl/save.",
622         order = 100)
623     public static void exportWithStylesheet(String fromID, String toID, String dirname, String style) {
624         Transformer transformer = getTransformer(style != null ? style + "-object" : null);
625         exportWith(fromID, toID, dirname, (content, out) -> {
626             StreamResult sr = new StreamResult(out);
627             JDOMSource doc = new JDOMSource(MCRXMLParserFactory.getNonValidatingParser().parseXML(content));
628             transformer.transform(doc, sr);
629         });
630     }
631 
632     /**
633      * Export any MCRObject's to files named <em>MCRObjectID</em>.xml in a directory named <em>dirname</em>.
634      * Exporting starts with <em>fromID</em> and ends with <em>toID</em>. IDs that aren't found will be skipped.
635      * The method use the content transformer <em>transname</em>xsl.
636      *
637      * @param fromID
638      *            the ID of the MCRObject from be save.
639      * @param toID
640      *            the ID of the MCRObject to be save.
641      * @param dirname
642      *            the filename to store the object
643      * @param transname
644      *            the name of the transformer
645      */
646     @MCRCommand(
647         syntax = "export objects from {0} to {1} to directory {2} with transformer {3}",
648         help = "Stores all MCRObjects with MCRObjectID's between {0} and {1} to the directory {2} "
649             + "with the transformer {3}.",
650         order = 100)
651     public static void exportWithTransformer(String fromID, String toID, String dirname, String transname) {
652         MCRContentTransformer transformer = MCRContentTransformerFactory.getTransformer(transname);
653         exportWith(fromID, toID, dirname, transformer::transform);
654     }
655 
656     private static void exportWith(String fromID, String toID, String dirname,
657         FailableBiConsumer<MCRContent, OutputStream, Exception> trans) {
658         MCRObjectID fid, tid;
659 
660         // check fromID and toID
661         try {
662             fid = MCRObjectID.getInstance(fromID);
663             tid = MCRObjectID.getInstance(toID);
664         } catch (Exception ex) {
665             LOGGER.error("FromID : {}", ex.getMessage());
666             return;
667         }
668         // check dirname
669         File dir = new File(dirname);
670         if (!dir.isDirectory()) {
671             LOGGER.error("{} is not a directory.", dirname);
672             return;
673         }
674 
675         int k = 0;
676         try {
677             for (int i = fid.getNumberAsInteger(); i < tid.getNumberAsInteger() + 1; i++) {
678                 String id = MCRObjectID.formatID(fid.getProjectId(), fid.getTypeId(), i);
679                 if (!MCRMetadataManager.exists(MCRObjectID.getInstance(id))) {
680                     continue;
681                 }
682                 if (!exportMCRObject(dir, trans, id)) {
683                     continue;
684                 }
685                 k++;
686             }
687         } catch (Exception ex) {
688             LOGGER.error(ex.getMessage());
689             LOGGER.error("Exception while store file to {}", dir.getAbsolutePath());
690             return;
691         }
692         LOGGER.info("{} Object's stored under {}.", k, dir.getAbsolutePath());
693     }
694 
695     /**
696      * Export all MCRObject's with data type <em>type</em> to files named <em>MCRObjectID</em>.xml in a directory
697      * named <em>dirname</em>. The method uses the converter stylesheet <em>style</em>.xsl.
698      *
699      * @param type
700      *            the MCRObjectID type
701      * @param dirname
702      *            the filename to store the object
703      * @param style
704      *            the type of the stylesheet
705      */
706     @MCRCommand(
707         syntax = "export all objects of type {0} to directory {1} with stylesheet {2}",
708         help = "Stores all MCRObjects of type {0} to directory {1} with the stylesheet {2}-object.xsl."
709             + "For {2}, the default is xsl/save.",
710         order = 120)
711     public static List<String> exportAllObjectsOfTypeWithStylesheet(String type, String dirname, String style) {
712         List<String> objectIds = MCRXMLMetadataManager.instance().listIDsOfType(type);
713         return buildExportCommands(new File(dirname), style, objectIds);
714     }
715 
716     /**
717      * Export all MCRObject's with data base <em>base</em> to files named <em>MCRObjectID</em>.xml in a directory
718      * named <em>dirname</em>. The method uses the converter stylesheet <em>style</em>.xsl.
719      *
720      * @param base
721      *            the MCRObjectID base
722      * @param dirname
723      *            the filename to store the object
724      * @param style
725      *            the type of the stylesheet
726      */
727     @MCRCommand(
728         syntax = "export all objects of base {0} to directory {1} with stylesheet {2}",
729         help = "Stores all MCRObjects of base {0} to directory {1} with the stylesheet {2}-object.xsl."
730             + " For {2}, the default is xsl/save.",
731         order = 130)
732     public static List<String> exportAllObjectsOfBaseWithStylesheet(String base, String dirname, String style) {
733         List<String> objectIds = MCRXMLMetadataManager.instance().listIDsForBase(base);
734         return buildExportCommands(new File(dirname), style, objectIds);
735     }
736 
737     private static List<String> buildExportCommands(File dir, String style, List<String> objectIds) {
738         if (dir.isFile()) {
739             LOGGER.error("{} is not a dirctory.", dir);
740             return Collections.emptyList();
741         }
742         List<String> cmds = new ArrayList<>(objectIds.size());
743         for (String id : objectIds) {
744             String command = new MessageFormat(EXPORT_OBJECT_TO_DIRECTORY_WITH_STYLESHEET_COMMAND, Locale.ROOT)
745                 .format(new Object[] { id, dir.getAbsolutePath(), style });
746             cmds.add(command);
747         }
748         return cmds;
749     }
750 
751     /**
752      * This method searches for the stylesheet <em>style</em>.xsl and builds the transformer. Default is
753      * <em>save-object.xsl</em> if no stylesheet is given or the stylesheet couldn't be resolved.
754      *
755      * @param style
756      *            the name of the style to be used when resolving the stylesheet
757      * @return the transformer
758      */
759     private static Transformer getTransformer(String style) {
760         return MCRCommandUtils.getTransformer(style, DEFAULT_STYLE, TRANSFORMER_CACHE);
761     }
762 
763     /**
764      * The method read a MCRObject and use a transformation to write the data to a file. There aren't any steps to
765      * handel errors and save the damaged data.
766      * <ul>
767      * <li>Read data for object ID in the MCRObject, add ACLs and store it as checked and transformed XML. Return true.
768      * </li>
769      * <li>If it can't find a transformer instance (no script file found) it store the checked data with ACLs native in
770      * the file. Warning and return true.</li>
771      * <li>If it get an exception while build the MCRObject, it try to read the XML blob and store it without check and
772      * ACLs to the file. Warning and return true.</li>
773      * <li>If it get an exception while store the native data without check, ACLs and transformation it return a
774      * warning and false.</li>
775      * </ul>
776      *
777      * @param dir
778      *            the file instance to store
779      * @param trans
780      *            the transformation
781      * @param nid
782      *            the MCRObjectID
783      * @return true if the store was okay (see description), else return false
784      * @throws IOException
785      * @throws MCRException
786      */
787     private static boolean exportMCRObject(File dir, FailableBiConsumer<MCRContent, OutputStream, Exception> trans,
788         String nid) throws IOException, MCRException {
789         MCRContent content;
790         try {
791             // if object doesn't exist - no exception is caught!
792             content = MCRXMLMetadataManager.instance().retrieveContent(MCRObjectID.getInstance(nid));
793         } catch (MCRException ex) {
794             return false;
795         }
796 
797         File xmlOutput = new File(dir, nid + ".xml");
798 
799         if (trans != null) {
800             FileOutputStream out = new FileOutputStream(xmlOutput);
801             try {
802                 trans.accept(content, out);
803             } catch (UncheckedIOException e) {
804                 throw e.getCause();
805             } catch (IOException | RuntimeException e) {
806                 throw e;
807             } catch (Exception e) {
808                 throw new MCRException(e);
809             }
810         } else {
811             content.sendTo(xmlOutput);
812         }
813         LOGGER.info("Object {} saved to {}.", nid, xmlOutput.getCanonicalPath());
814         return true;
815     }
816 
817     /**
818      * Get the next free MCRObjectID for the given MCRObjectID base.
819      *
820      * @param base
821      *            the MCRObjectID base string
822      */
823     @MCRCommand(
824         syntax = "get next ID for base {0}",
825         help = "Returns the next free MCRObjectID for the ID base {0}.",
826         order = 150)
827     public static void getNextID(String base) {
828         try {
829             LOGGER.info(MCRObjectID.getNextFreeId(base));
830         } catch (MCRException ex) {
831             LOGGER.error(ex.getMessage());
832         }
833     }
834 
835     /**
836      * Get the last used MCRObjectID for the given MCRObjectID base.
837      *
838      * @param base
839      *            the MCRObjectID base string
840      */
841 
842     @MCRCommand(
843         syntax = "get last ID for base {0}",
844         help = "Returns the last used MCRObjectID for the ID base {0}.",
845         order = 140)
846     public static void getLastID(String base) {
847         LOGGER.info(MCRObjectID.getLastID(base));
848     }
849 
850     /**
851      * List all selected MCRObjects.
852      */
853     @MCRCommand(
854         syntax = "list selected",
855         help = "Prints the id of selected objects",
856         order = 190)
857     public static void listSelected() {
858         LOGGER.info("List selected MCRObjects");
859         if (getSelectedObjectIDs().isEmpty()) {
860             LOGGER.info("No Resultset to work with, use command \"select objects with solr query {0} in core {1}\"" +
861                 " or \"select objects with xpath {0}\" to build one");
862             return;
863         }
864         StringBuilder out = new StringBuilder();
865         for (String id : getSelectedObjectIDs()) {
866             out.append(id).append(" ");
867         }
868         LOGGER.info(out.toString());
869     }
870 
871     /**
872      * List revisions of an MyCoRe Object.
873      *
874      * @param id
875      *            id of MyCoRe Object
876      */
877     @MCRCommand(
878         syntax = "list revisions of {0}",
879         help = "List revisions of MCRObject.",
880         order = 260)
881     public static void listRevisions(String id) {
882         MCRObjectID mcrId = MCRObjectID.getInstance(id);
883         SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ROOT);
884         try {
885             StringBuilder log = new StringBuilder("Revisions:\n");
886             List<? extends MCRAbstractMetadataVersion<?>> revisions = MCRXMLMetadataManager.instance()
887                 .listRevisions(mcrId);
888             for (MCRAbstractMetadataVersion<?> revision : revisions) {
889                 log.append(revision.getRevision()).append(" ");
890                 log.append(revision.getType()).append(" ");
891                 log.append(sdf.format(revision.getDate())).append(" ");
892                 log.append(revision.getUser());
893                 log.append("\n");
894             }
895             LOGGER.info(log.toString());
896         } catch (Exception exc) {
897             LOGGER.error("While print revisions.", exc);
898         }
899     }
900 
901     /**
902      * This method restores a MyCoRe Object to the selected revision. Please note that children and derivates are not
903      * deleted or reverted!
904      *
905      * @param id
906      *            id of MyCoRe Object
907      * @param revision
908      *            revision to restore
909      */
910     @MCRCommand(
911         syntax = "restore {0} to revision {1}",
912         help = "Restores the selected MCRObject to the selected revision.",
913         order = 270)
914     public static void restoreToRevision(String id, String revision) {
915         LOGGER.info("Try to restore object {} with revision {}", id, revision);
916         MCRObjectID mcrId = MCRObjectID.getInstance(id);
917         try {
918             MCRObjectUtils.restore(mcrId, revision);
919             LOGGER.info("Object {} successfully restored!", id);
920         } catch (Exception exc) {
921             LOGGER.error("While retrieving object {} with revision {}", id, revision, exc);
922         }
923     }
924 
925     /**
926      * Does a xsl transform with the given mycore object.
927      * <p>
928      * To use this command create a new xsl file and copy following xslt code into it.
929      * </p>
930      *
931      * <pre>
932      * {@code
933      * <?xml version="1.0" encoding="utf-8"?>
934      * <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
935      *
936      *   <xsl:template match='@*|node()'>
937      *     <!-- default template: just copy -->
938      *     <xsl:copy>
939      *       <xsl:apply-templates select='@*|node()' />
940      *     </xsl:copy>
941      *   </xsl:template>
942      *
943      * </xsl:stylesheet>
944      * }
945      * </pre>
946      * <p>
947      * Insert a new template match, for example:
948      * </p>
949      *
950      * <pre>
951      * {@code
952      * <xsl:template match="metadata/mainTitle/@heritable">
953      *   <xsl:attribute name="heritable"><xsl:value-of select="'true'"/></xsl:attribute>
954      * </xsl:template>
955      * }
956      * </pre>
957      *
958      * @param objectId
959      *            object to transform
960      * @param xslFilePath
961      *            path to xsl file
962      * @throws MCRPersistenceException see {@link MCRMetadataManager#update(MCRObject)}
963      * @throws MCRAccessException see {@link MCRMetadataManager#update(MCRObject)}
964      */
965     @MCRCommand(
966         syntax = "xslt {0} with file {1}",
967         help = "transforms a mycore object {0} with the given file or URI {1}",
968         order = 280)
969     public static void xslt(String objectId, String xslFilePath) throws IOException, JDOMException, SAXException,
970         TransformerException, MCRPersistenceException, MCRAccessException,
971         ParserConfigurationException {
972         xslt(objectId, xslFilePath, false);
973     }
974 
975     /**
976      * @see #xslt(String, String)
977      *
978      * Forces the xml to overwrite even if the root name of the original and the result differs.
979      *
980      * @param objectId
981      *            object to transform
982      * @param xslFilePath
983      *            path to xsl file
984      * @throws MCRPersistenceException see {@link MCRMetadataManager#update(MCRObject)}
985      * @throws MCRAccessException see {@link MCRMetadataManager#update(MCRObject)}
986      */
987     @MCRCommand(
988         syntax = "force xslt {0} with file {1}",
989         help = "transforms a mycore object {0} with the given file or URI {1}. Overwrites anyway if original "
990             + "root name and result root name are different.",
991         order = 285)
992     public static void forceXSLT(String objectId, String xslFilePath) throws IOException, JDOMException, SAXException,
993         TransformerException, MCRPersistenceException, MCRAccessException,
994         ParserConfigurationException {
995         xslt(objectId, xslFilePath, true);
996     }
997 
998     private static void xslt(String objectId, String xslFilePath, boolean force) throws IOException, JDOMException,
999         SAXException, TransformerException, MCRPersistenceException, MCRAccessException, ParserConfigurationException {
1000         File xslFile = new File(xslFilePath);
1001         Source xslSource;
1002         if (xslFile.exists()) {
1003             xslSource = new StreamSource(xslFile);
1004         } else {
1005             xslSource = MCRURIResolver.instance().resolve(xslFilePath, null);
1006             if (xslSource == null) {
1007                 xslSource = new StreamSource(xslFilePath);
1008             }
1009         }
1010         MCRSourceContent style = new MCRSourceContent(xslSource);
1011         MCRObjectID mcrId = MCRObjectID.getInstance(objectId);
1012         Document document = MCRXMLMetadataManager.instance().retrieveXML(mcrId);
1013         // do XSL transform
1014         TransformerFactory transformerFactory = TransformerFactory.newInstance();
1015         transformerFactory.setErrorListener(MCRErrorListener.getInstance());
1016         transformerFactory.setURIResolver(MCRURIResolver.instance());
1017         XMLReader xmlReader = MCRXMLParserFactory.getNonValidatingParser().getXMLReader();
1018         xmlReader.setEntityResolver(MCREntityResolver.instance());
1019         SAXSource styleSource = new SAXSource(xmlReader, style.getInputSource());
1020         Transformer transformer = transformerFactory.newTransformer(styleSource);
1021         for (Entry<String, String> property : MCRConfiguration2.getPropertiesMap().entrySet()) {
1022             transformer.setParameter(property.getKey(), property.getValue());
1023         }
1024         transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
1025         transformer.setOutputProperty(OutputKeys.INDENT, "no");
1026         JDOMResult result = new JDOMResult();
1027         transformer.transform(new JDOMSource(document), result);
1028         Document resultDocument = Objects.requireNonNull(result.getDocument(), "Could not get transformation result");
1029 
1030         String originalName = document.getRootElement().getName();
1031         String resultName = resultDocument.getRootElement().getName();
1032         if (!force && !originalName.equals(resultName)) {
1033             LOGGER.error("{}: root name '{}' does not match result name '{}'.", objectId, originalName, resultName);
1034             return;
1035         }
1036 
1037         // update on diff
1038         if (MCRXMLHelper.deepEqual(document, resultDocument)) {
1039             return;
1040         }
1041         switch (resultName) {
1042         case MCRObject.ROOT_NAME:
1043             MCRMetadataManager.update(new MCRObject(resultDocument));
1044             break;
1045         case MCRDerivate.ROOT_NAME:
1046             MCRMetadataManager.update(new MCRDerivate(resultDocument));
1047             break;
1048         default:
1049             LOGGER.error("Unable to transform '{}' because unknown result root name '{}'.", objectId, resultName);
1050             break;
1051         }
1052     }
1053 
1054     @MCRCommand(syntax = "transform object {0} with transformer {1}",
1055         help = "Transforms the object with the id {0} using the transformer with the id {1} and" +
1056                 " updates the object with the result")
1057     public static void transformObject(String objectIDStr, String transformer)
1058         throws IOException, JDOMException, SAXException, MCRAccessException {
1059         MCRObjectID objectID = MCRObjectID.getInstance(objectIDStr);
1060         if (!MCRMetadataManager.exists(objectID)) {
1061             LOGGER.error("The object {} does not exist!", objectID);
1062             return;
1063         }
1064 
1065         MCRObject mcrObject = MCRMetadataManager.retrieveMCRObject(objectID);
1066 
1067         MCRBaseContent baseContent = new MCRBaseContent(mcrObject);
1068         MCRContent result = new MCRLayoutTransformerFactory().getTransformer(transformer).transform(baseContent);
1069         Document resultXML = result.asXML();
1070         MCRObject resulting = new MCRObject(resultXML);
1071 
1072         if (!MCRXMLHelper.deepEqual(mcrObject.createXML(), resultXML)) {
1073             LOGGER.info("The Object changed with the transformation. Execute Update.");
1074             MCRMetadataManager.update(resulting);
1075         } else {
1076             LOGGER.info("The Object did not change with the transformation. Skip Update.");
1077         }
1078     }
1079 
1080     /**
1081      * Moves object to new parent.
1082      *
1083      * @param sourceId
1084      *            object that should be attached to new parent
1085      * @param newParentId
1086      *            the ID of the new parent
1087      * @throws MCRAccessException see {@link MCRMetadataManager#update(MCRObject)}
1088      */
1089     @MCRCommand(
1090         syntax = "set parent of {0} to {1}",
1091         help = "replaces a parent of an object (first parameter) to the given new one (second parameter)",
1092         order = 300)
1093     public static void replaceParent(String sourceId, String newParentId) throws MCRPersistenceException,
1094         MCRAccessException {
1095         // child
1096         MCRObject sourceMCRObject = MCRMetadataManager.retrieveMCRObject(MCRObjectID.getInstance(sourceId));
1097         // old parent
1098         MCRObjectID oldParentId = sourceMCRObject.getStructure().getParentID();
1099         MCRObjectID newParentObjectID = MCRObjectID.getInstance(newParentId);
1100 
1101         if (newParentObjectID.equals(oldParentId)) {
1102             LOGGER.info("Object {} is already child of {}", sourceId, newParentId);
1103             return;
1104         }
1105 
1106         MCRObject oldParentMCRObject = null;
1107 
1108         if (oldParentId != null) {
1109             try {
1110                 oldParentMCRObject = MCRMetadataManager.retrieveMCRObject(oldParentId);
1111             } catch (Exception exc) {
1112                 LOGGER.error("Unable to get old parent object {}, its probably deleted.", oldParentId, exc);
1113             }
1114         }
1115 
1116         // change href to new parent
1117         LOGGER.info("Setting link in \"{}\" to parent \"{}\"", sourceId, newParentObjectID);
1118         MCRMetaLinkID parentLinkId = new MCRMetaLinkID("parent", 0);
1119         parentLinkId.setReference(newParentObjectID, null, null);
1120         sourceMCRObject.getStructure().setParent(parentLinkId);
1121 
1122         if (oldParentMCRObject != null) {
1123             // remove Child in old parent
1124             LOGGER.info("Remove child \"{}\" in old parent \"{}\"", sourceId, oldParentId);
1125             oldParentMCRObject.getStructure().removeChild(sourceMCRObject.getId());
1126 
1127             LOGGER.info("Update old parent \"{}\n", oldParentId);
1128             MCRMetadataManager.update(oldParentMCRObject);
1129         }
1130 
1131         LOGGER.info("Update \"{}\" in datastore (saving new link)", sourceId);
1132         MCRMetadataManager.update(sourceMCRObject);
1133         if (LOGGER.isDebugEnabled()) {
1134             LOGGER.debug("Structure: {}", sourceMCRObject.getStructure().isValid());
1135             LOGGER.debug("Object: {}", sourceMCRObject.isValid());
1136         }
1137     }
1138 
1139     /**
1140      * Check the derivate links in objects of MCR base ID for existing. It looks to the XML store on the disk to get all
1141      * object IDs.
1142      *
1143      * @param baseId
1144      *            the base part of a MCRObjectID e.g. DocPortal_document
1145      */
1146     @MCRCommand(
1147         syntax = "check derivate entries in objects for base {0}",
1148         help = "check in all objects with MCR base ID {0} for existing linked derivates",
1149         order = 400)
1150     public static void checkDerivatesInObjects(String baseId) {
1151         if (baseId == null || baseId.length() == 0) {
1152             LOGGER.error("Base ID missed for check derivate entries in objects for base {0}");
1153             return;
1154         }
1155         MCRXMLMetadataManager mgr = MCRXMLMetadataManager.instance();
1156         List<String> idList = mgr.listIDsForBase(baseId);
1157         int counter = 0;
1158         int maxresults = idList.size();
1159         for (String objid : idList) {
1160             counter++;
1161             LOGGER.info("Processing dataset {} from {} with ID: {}", counter, maxresults, objid);
1162             // get from data
1163             MCRObjectID mcrobjid = MCRObjectID.getInstance(objid);
1164             MCRObject obj = MCRMetadataManager.retrieveMCRObject(mcrobjid);
1165             List<MCRMetaEnrichedLinkID> derivateEntries = obj.getStructure().getDerivates();
1166             for (MCRMetaLinkID derivateEntry : derivateEntries) {
1167                 String derid = derivateEntry.getXLinkHref();
1168                 if (!mgr.exists(MCRObjectID.getInstance(derid))) {
1169                     LOGGER.error("   !!! Missing derivate {} in database for base ID {}", derid, baseId);
1170                 }
1171             }
1172         }
1173         LOGGER.info("Check done for {} entries", Integer.toString(counter));
1174     }
1175 
1176     /**
1177      * Checks objects of the specified base ID for validity against their specified schemas.
1178      *
1179      * @param baseID
1180      *            the base part of a MCRObjectID e.g. DocPortal_document
1181      */
1182     @MCRCommand(
1183         syntax = "validate object schema for base {0}",
1184         help = "Validates all objects of base {0} against their specified schema.",
1185         order = 401)
1186     public static List<String> validateObjectsOfBase(String baseID) {
1187         return MCRCommandUtils.getIdsForBaseId(baseID)
1188             .map(id -> "validate object schema for ID " + id)
1189             .collect(Collectors.toList());
1190     }
1191 
1192     /**
1193      * Checks objects of the specified type for validity against their specified schemas.
1194      *
1195      * @param type
1196      *            the type of a MCRObjectID e.g. document
1197      */
1198     @MCRCommand(
1199         syntax = "validate object schema for type {0}",
1200         help = "Validates all object of type {0} against their specified schema.",
1201         order = 402)
1202     public static List<String> validateObjectsOfType(String type) {
1203         return MCRCommandUtils.getIdsForType(type)
1204             .map(id -> "validate object schema for ID " + id)
1205             .collect(Collectors.toList());
1206     }
1207 
1208     /**
1209      * Check if an object validates against its specified schema.
1210      *
1211      * @param objectID
1212      *            the ID of an object to check
1213      */
1214     @MCRCommand(
1215         syntax = "validate object schema for ID {0}",
1216         help = "Checks if object {0} validates against its specified schema.",
1217         order = 404)
1218     public static void validateObject(String objectID) {
1219         validateObjectWithTransformer(objectID, null);
1220     }
1221 
1222     /**
1223      * Check if an object validates against its specified schema.
1224      *
1225      * @param objectID
1226      *            the ID of an object to check
1227      * @param transformerType
1228      *            the name of a stylesheet that the object should be transformed with before validation
1229      */
1230     @MCRCommand(
1231         syntax = "validate object schema for ID {0} after transformer {1}",
1232         help = "Checks if object {0} validates against its specified schema, "
1233             + "after being transformed through {1}.xsl.",
1234         order = 403)
1235     public static void validateObjectWithTransformer(String objectID, String transformerType) {
1236         if (objectID == null || objectID.length() == 0) {
1237             throw new MCRException("ID of an object required to check its schema validity.");
1238         }
1239         LOGGER.info("validate object schema for ID " + objectID);
1240         Transformer trafo = null;
1241         if (transformerType != null) {
1242             // getTransformer with non-existent input successfully returns a working transformer
1243             // that "successfully transforms", an error would be preferable 
1244             trafo = getTransformer(transformerType);
1245             LOGGER.debug("Transformer {} has been loaded.", transformerType);
1246         }
1247         MCRObjectID objID = MCRObjectID.getInstance(objectID);
1248         try {
1249             doValidateObjectAgainstSchema(objID, trafo);
1250             LOGGER.info("Object {} successfully validated.", objectID);
1251         } catch (MCRException e) {
1252             LOGGER.error("Object {} failed its validation!", objectID);
1253             throw e;
1254         }
1255     }
1256 
1257     private static void doValidateObjectAgainstSchema(MCRObjectID objID, Transformer trans) {
1258         // MCRMetadataManager -> retrieveMCRObject() -> MCRObject.createXML already validates the contents
1259         // we need to offer transformation first though, so manual talking to MCRXMLMetadataManager
1260         // for the object contents, then manually using a validating XML parser later
1261         MCRXMLMetadataManager mgr = MCRXMLMetadataManager.instance();
1262         Document doc;
1263         try {
1264             doc = mgr.retrieveXML(objID);
1265         } catch (IOException | JDOMException | SAXException e) {
1266             throw new MCRException(
1267                 "Object " + objID.toString() + " could not be retrieved, unable to validate against schema!", e);
1268         }
1269         if (doc == null) {
1270             throw new MCRException("Could not get object " + objID.toString() + " from XML store");
1271         }
1272         MCRObject object = new MCRObject(doc);
1273         try {
1274             object.validate();
1275         } catch (MCRException e) {
1276             throw new MCRException(
1277                 "Object " + objID.toString()
1278                     + " does not pass basic self-validation, unable to validate against schema!",
1279                 e);
1280         }
1281         String schema = object.getSchema();
1282         if (schema == null) {
1283             throw new MCRException(
1284                 "Object " + objID.toString() + " has no assigned schema, unable to validate against it!");
1285         }
1286         if (trans != null) {
1287             JDOMResult res = new JDOMResult();
1288             try {
1289                 trans.transform(new JDOMSource(doc), res);
1290                 doc = Objects.requireNonNull(res.getDocument(), "Could not get transformation result");
1291             } catch (TransformerException | MCRException | NullPointerException e) {
1292                 throw new MCRException("Object " + objID.toString()
1293                     + " could not be transformed, unable to validate against schema!",
1294                     e);
1295             }
1296             LOGGER.info("Object {} successfully transformed.", objID.toString());
1297         }
1298         try {
1299             MCRXMLParserFactory.getValidatingParser().parseXML(new MCRJDOMContent(doc));
1300         } catch (MCRException | SAXException e) {
1301             throw new MCRException("Object " + objID.toString() + " failed to parse against its schema!", e);
1302         }
1303     }
1304 
1305     @MCRCommand(
1306         syntax = "execute for selected {0}",
1307         help = "Calls the given command multiple times for all selected objects." +
1308             " The replacement is defined by an {x}.E.g. 'execute for selected set" +
1309             " parent of {x} to myapp_container_00000001'",
1310         order = 450)
1311     public static List<String> executeForSelected(String command) {
1312         if (!command.contains("{x}")) {
1313             LOGGER.info("No replacement defined. Use the {x} variable in order to execute your command with all "
1314                 + "selected objects.");
1315             return Collections.emptyList();
1316         }
1317         return getSelectedObjectIDs().stream()
1318             .map(objID -> command.replaceAll("\\{x}", objID))
1319             .collect(Collectors.toList());
1320     }
1321 
1322     /**
1323      * The method start the repair of the metadata search for a given MCRObjectID type.
1324      *
1325      * @param type
1326      *            the MCRObjectID type
1327      */
1328     @MCRCommand(
1329         syntax = "repair metadata search of type {0}",
1330         help = "Scans the metadata store for MCRObjects of type {0} and restores them in the search store.",
1331         order = 170)
1332     public static List<String> repairMetadataSearch(String type) {
1333         LOGGER.info("Start the repair for type {}", type);
1334         return MCRCommandUtils.getIdsForType(type)
1335             .map(id -> "repair metadata search of ID " + id)
1336             .collect(Collectors.toList());
1337     }
1338 
1339     /**
1340      * The method start the repair of the metadata search for a given MCRObjectID base.
1341      *
1342      * @param baseID
1343      *            the base part of a MCRObjectID e.g. DocPortal_document
1344      */
1345     @MCRCommand(
1346         syntax = "repair metadata search of base {0}",
1347         help = "Scans the metadata store for MCRObjects of base {0} and restores them in the search store.",
1348         order = 171)
1349     public static List<String> repairMetadataSearchForBase(String baseID) {
1350         LOGGER.info("Start the repair for base {}", baseID);
1351         return MCRCommandUtils.getIdsForBaseId(baseID)
1352             .map(id -> "repair metadata search of ID " + id)
1353             .collect(Collectors.toList());
1354     }
1355 
1356     /**
1357      * The method start the repair of the metadata search for a given MCRObjectID as String.
1358      *
1359      * @param id
1360      *            the MCRObjectID as String
1361      */
1362     @MCRCommand(
1363         syntax = "repair metadata search of ID {0}",
1364         help = "Retrieves the MCRObject with the MCRObjectID {0} and restores it in the search store.",
1365         order = 180)
1366     public static void repairMetadataSearchForID(String id) {
1367         LOGGER.info("Start the repair for the ID {}", id);
1368         if (!MCRObjectID.isValid(id)) {
1369             LOGGER.error("The String {} is not a MCRObjectID.", id);
1370             return;
1371         }
1372         MCRObjectID mid = MCRObjectID.getInstance(id);
1373         MCRBase obj = MCRMetadataManager.retrieve(mid);
1374         MCRMetadataManager.fireRepairEvent(obj);
1375         LOGGER.info("Repaired {}", mid);
1376     }
1377 
1378     @MCRCommand(
1379         syntax = "repair mcrlinkhref table",
1380         help = "Runs through the whole table and checks for already deleted mcr objects and deletes them.",
1381         order = 185)
1382     public static void repairMCRLinkHrefTable() {
1383         EntityManager em = MCREntityManagerProvider.getCurrentEntityManager();
1384         TypedQuery<String> fromQuery = em.createQuery("SELECT DISTINCT m.key.mcrfrom FROM MCRLINKHREF m", String.class);
1385         TypedQuery<String> toQuery = em.createQuery("SELECT DISTINCT m.key.mcrto FROM MCRLINKHREF m", String.class);
1386         String query = "DELETE FROM MCRLINKHREF m WHERE m.key.mcrfrom IN (:invalidIds) or m.key.mcrto IN (:invalidIds)";
1387         // open streams
1388         try (Stream<String> fromStream = fromQuery.getResultStream()) {
1389             try (Stream<String> toStream = toQuery.getResultStream()) {
1390                 List<String> invalidIds = Stream.concat(fromStream, toStream)
1391                     .distinct()
1392                     .filter(MCRObjectID::isValid)
1393                     .map(MCRObjectID::getInstance)
1394                     .filter(MCRStreamUtils.not(MCRMetadataManager::exists))
1395                     .map(MCRObjectID::toString)
1396                     .collect(Collectors.toList());
1397                 // delete
1398                 em.createQuery(query).setParameter("invalidIds", invalidIds).executeUpdate();
1399             }
1400         }
1401     }
1402 
1403     @MCRCommand(
1404         syntax = "rebuild mcrlinkhref table for object {0}",
1405         help = "Rebuilds (remove/create) all entries of the link href table for the given object id.",
1406         order = 188)
1407     public static void rebuildMCRLinkHrefTableForObject(String objectId) {
1408         MCRLinkTableManager.instance().update(MCRObjectID.getInstance(objectId));
1409     }
1410 
1411 }