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.restapi.v1.utils;
20  
21  import java.io.IOException;
22  import java.io.StringWriter;
23  import java.net.URLDecoder;
24  import java.nio.charset.StandardCharsets;
25  import java.nio.file.DirectoryStream;
26  import java.nio.file.FileVisitOption;
27  import java.nio.file.Files;
28  import java.nio.file.Path;
29  import java.nio.file.attribute.BasicFileAttributes;
30  import java.text.ParseException;
31  import java.text.SimpleDateFormat;
32  import java.util.ArrayList;
33  import java.util.Date;
34  import java.util.EnumSet;
35  import java.util.HashSet;
36  import java.util.List;
37  import java.util.Locale;
38  import java.util.Set;
39  import java.util.concurrent.TimeUnit;
40  import java.util.stream.Collectors;
41  
42  import org.apache.logging.log4j.LogManager;
43  import org.apache.logging.log4j.Logger;
44  import org.apache.solr.client.solrj.SolrClient;
45  import org.apache.solr.client.solrj.SolrQuery;
46  import org.apache.solr.client.solrj.SolrServerException;
47  import org.apache.solr.client.solrj.response.QueryResponse;
48  import org.apache.solr.common.SolrDocumentList;
49  import org.jdom2.Comment;
50  import org.jdom2.Document;
51  import org.jdom2.Element;
52  import org.jdom2.JDOMException;
53  import org.jdom2.filter.Filters;
54  import org.jdom2.output.Format;
55  import org.jdom2.output.XMLOutputter;
56  import org.jdom2.xpath.XPathExpression;
57  import org.jdom2.xpath.XPathFactory;
58  import org.mycore.common.MCRConstants;
59  import org.mycore.common.MCRException;
60  import org.mycore.common.config.MCRConfiguration2;
61  import org.mycore.common.content.MCRJDOMContent;
62  import org.mycore.common.content.transformer.MCRContentTransformer;
63  import org.mycore.common.content.transformer.MCRContentTransformerFactory;
64  import org.mycore.datamodel.common.MCRObjectIDDate;
65  import org.mycore.datamodel.common.MCRXMLMetadataManager;
66  import org.mycore.datamodel.metadata.MCRDerivate;
67  import org.mycore.datamodel.metadata.MCRMetaClassification;
68  import org.mycore.datamodel.metadata.MCRMetaLinkID;
69  import org.mycore.datamodel.metadata.MCRMetadataManager;
70  import org.mycore.datamodel.metadata.MCRObject;
71  import org.mycore.datamodel.metadata.MCRObjectID;
72  import org.mycore.datamodel.niofs.MCRPath;
73  import org.mycore.datamodel.niofs.MCRPathXML;
74  import org.mycore.frontend.jersey.MCRJerseyUtil;
75  import org.mycore.restapi.v1.MCRRestAPIObjects;
76  import org.mycore.restapi.v1.errors.MCRRestAPIError;
77  import org.mycore.restapi.v1.errors.MCRRestAPIException;
78  import org.mycore.restapi.v1.utils.MCRRestAPISortObject.SortOrder;
79  import org.mycore.solr.MCRSolrClientFactory;
80  import org.xml.sax.SAXException;
81  
82  import com.google.gson.stream.JsonWriter;
83  
84  import jakarta.ws.rs.core.Application;
85  import jakarta.ws.rs.core.CacheControl;
86  import jakarta.ws.rs.core.Request;
87  import jakarta.ws.rs.core.Response;
88  import jakarta.ws.rs.core.UriInfo;
89  import jakarta.ws.rs.core.Response.ResponseBuilder;
90  import jakarta.ws.rs.core.Response.Status;
91  
92  /**
93   * main utility class that handles REST requests
94   * 
95   * to filter the XML output of showMCRObject, set the properties:
96   * MCR.RestAPI.v1.Filter.XML
97   *   to your ContentTransformer-ID,
98   * MCR.ContentTransformer.[your ContentTransformer-ID here].Class
99   *   to your ContentTransformer's class and
100  * MCR.ContentTransformer.[your ContentTransformer-ID here].Stylesheet
101  *   to your filtering stylesheet.
102  * 
103  * @author Robert Stephan
104  * @author Christoph Neidahl
105  * 
106  * @version $Revision: $ $Date: $
107  */
108 public class MCRRestAPIObjectsHelper {
109 
110     private static final String GENERAL_ERROR_MSG = "A problem occured while fetching the data.";
111 
112     private static Logger LOGGER = LogManager.getLogger(MCRRestAPIObjectsHelper.class);
113 
114     private static SimpleDateFormat SDF_UTC = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
115 
116     public static Response showMCRObject(String pathParamId, String queryParamStyle, UriInfo info, Application app)
117         throws MCRRestAPIException {
118 
119         MCRObject mcrObj = retrieveMCRObject(pathParamId);
120         Document doc = mcrObj.createXML();
121         Element eStructure = doc.getRootElement().getChild("structure");
122 
123         if (queryParamStyle != null && !MCRRestAPIObjects.STYLE_DERIVATEDETAILS.equals(queryParamStyle)) {
124             throw new MCRRestAPIException(Response.Status.BAD_REQUEST,
125                 new MCRRestAPIError(MCRRestAPIError.CODE_WRONG_PARAMETER,
126                     "The value of parameter {style} is not allowed.",
127                     "Allowed values for {style} parameter are: " + MCRRestAPIObjects.STYLE_DERIVATEDETAILS));
128         }
129 
130         if (MCRRestAPIObjects.STYLE_DERIVATEDETAILS.equals(queryParamStyle) && eStructure != null) {
131             Element eDerObjects = eStructure.getChild("derobjects");
132             if (eDerObjects != null) {
133                 for (Element eDer : eDerObjects.getChildren("derobject")) {
134                     String derID = eDer.getAttributeValue("href", MCRConstants.XLINK_NAMESPACE);
135                     Element currentDerElement = eDer;
136                     try {
137                         MCRDerivate der = MCRMetadataManager.retrieveMCRDerivate(MCRObjectID.getInstance(derID));
138                         eDer.addContent(der.createXML().getRootElement().detach());
139 
140                         //<mycorederivate xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xlink="http://www.w3.org/1999/xlink" xsi:noNamespaceSchemaLocation="datamodel-derivate.xsd" ID="cpr_derivate_00003760" label="display_image" version="1.3">
141                         //  <derivate display="true">
142 
143                         currentDerElement = eDer.getChild("mycorederivate").getChild("derivate");
144                         Document docContents = listDerivateContentAsXML(
145                             MCRMetadataManager.retrieveMCRDerivate(MCRObjectID.getInstance(derID)), "/", -1, info, app);
146                         if (docContents.hasRootElement()) {
147                             eDer.addContent(docContents.getRootElement().detach());
148                         }
149                     } catch (MCRException e) {
150                         currentDerElement.addContent(new Comment("Error: Derivate not found."));
151                     } catch (IOException e) {
152                         currentDerElement
153                             .addContent(new Comment("Error: Derivate content could not be listed: " + e.getMessage()));
154                     }
155                 }
156             }
157         }
158 
159         StringWriter sw = new StringWriter();
160         XMLOutputter outputter = new XMLOutputter(Format.getPrettyFormat());
161         try {
162             String filterId = MCRConfiguration2.getString("MCR.RestAPI.v1.Filter.XML").orElse("");
163             if (filterId.length() > 0) {
164                 MCRContentTransformer trans = MCRContentTransformerFactory.getTransformer(filterId);
165                 Document filteredDoc = trans.transform(new MCRJDOMContent(doc)).asXML();
166                 outputter.output(filteredDoc, sw);
167             } else {
168                 outputter.output(doc, sw);
169             }
170         } catch (SAXException | JDOMException e) {
171             throw new MCRRestAPIException(Response.Status.INTERNAL_SERVER_ERROR, new MCRRestAPIError(
172                 MCRRestAPIError.CODE_INTERNAL_ERROR, "Unable to transform MCRContent to XML document", e.getMessage()));
173         } catch (IOException e) {
174             throw new MCRRestAPIException(Response.Status.INTERNAL_SERVER_ERROR, new MCRRestAPIError(
175                 MCRRestAPIError.CODE_INTERNAL_ERROR, "Unable to retrieve/transform MyCoRe object", e.getMessage()));
176         }
177 
178         return Response.ok(sw.toString())
179             .type("application/xml")
180             .build();
181     }
182 
183     public static Response showMCRDerivate(String pathParamMcrID, String pathParamDerID, UriInfo info, Application app,
184         boolean withDetails) throws MCRRestAPIException {
185 
186         MCRObjectID mcrObj = MCRObjectID.getInstance(pathParamMcrID);
187         MCRDerivate derObj = retrieveMCRDerivate(mcrObj, pathParamDerID);
188 
189         try {
190             Document doc = derObj.createXML();
191             if (withDetails) {
192                 Document docContent = listDerivateContentAsXML(derObj, "/", -1, info, app);
193                 if (docContent != null && docContent.hasRootElement()) {
194                     doc.getRootElement().addContent(docContent.getRootElement().detach());
195                 }
196             }
197 
198             StringWriter sw = new StringWriter();
199             XMLOutputter outputter = new XMLOutputter(Format.getPrettyFormat());
200             outputter.output(doc, sw);
201 
202             return Response.ok(sw.toString())
203                 .type("application/xml")
204                 .build();
205         } catch (IOException e) {
206             throw new MCRRestAPIException(Response.Status.INTERNAL_SERVER_ERROR,
207                 new MCRRestAPIError(MCRRestAPIError.CODE_INTERNAL_ERROR, GENERAL_ERROR_MSG, e.getMessage()));
208         }
209 
210         // return MCRRestAPIError.create(Response.Status.INTERNAL_SERVER_ERROR, "Unexepected program flow termination.",
211         //       "Please contact a developer!").createHttpResponse();
212     }
213 
214     private static String listDerivateContentAsJson(MCRDerivate derObj, String path, int depth, UriInfo info,
215         Application app)
216         throws IOException {
217         StringWriter sw = new StringWriter();
218         MCRPath root = MCRPath.getPath(derObj.getId().toString(), "/");
219         root = MCRPath.toMCRPath(root.resolve(path));
220         if (depth == -1) {
221             depth = Integer.MAX_VALUE;
222         }
223         if (root != null) {
224             JsonWriter writer = new JsonWriter(sw);
225             Files.walkFileTree(root, EnumSet.noneOf(FileVisitOption.class), depth,
226                 new MCRJSONFileVisitor(writer, derObj.getOwnerID(), derObj.getId(), info, app));
227             writer.close();
228         }
229         return sw.toString();
230     }
231 
232     private static Document listDerivateContentAsXML(MCRDerivate derObj, String path, int depth, UriInfo info,
233         Application app)
234         throws IOException {
235         Document doc = new Document();
236 
237         MCRPath root = MCRPath.getPath(derObj.getId().toString(), "/");
238         root = MCRPath.toMCRPath(root.resolve(path));
239         if (depth == -1) {
240             depth = Integer.MAX_VALUE;
241         }
242         if (root != null) {
243             Element eContents = new Element("contents");
244             eContents.setAttribute("mycoreobject", derObj.getOwnerID().toString());
245             eContents.setAttribute("mycorederivate", derObj.getId().toString());
246             doc.addContent(eContents);
247             if (!path.endsWith("/")) {
248                 path += "/";
249             }
250             MCRPath p = MCRPath.getPath(derObj.getId().toString(), path);
251             if (p != null && Files.exists(p)) {
252                 Element eRoot = MCRPathXML.getDirectoryXML(p).getRootElement();
253                 eContents.addContent(eRoot.detach());
254                 createXMLForSubdirectories(p, eRoot, 1, depth);
255             }
256 
257             //add href Attributes
258             String baseURL = MCRJerseyUtil.getBaseURL(info, app)
259                 + MCRConfiguration2.getStringOrThrow("MCR.RestAPI.v1.Files.URL.path");
260             baseURL = baseURL.replace("${mcrid}", derObj.getOwnerID().toString()).replace("${derid}",
261                 derObj.getId().toString());
262             XPathExpression<Element> xp = XPathFactory.instance().compile(".//child[@type='file']", Filters.element());
263             for (Element e : xp.evaluate(eContents)) {
264                 String uri = e.getChildText("uri");
265                 if (uri != null) {
266                     int pos = uri.lastIndexOf(":/");
267                     String subPath = uri.substring(pos + 2);
268                     while (subPath.startsWith("/")) {
269                         subPath = path.substring(1);
270                     }
271                     e.setAttribute("href", baseURL + subPath);
272                 }
273             }
274         }
275         return doc;
276     }
277 
278     private static void createXMLForSubdirectories(MCRPath mcrPath, Element currentElement, int currentDepth,
279         int maxDepth) {
280         if (currentDepth < maxDepth) {
281             XPathExpression<Element> xp = XPathFactory.instance().compile("./children/child[@type='directory']",
282                 Filters.element());
283             for (Element e : xp.evaluate(currentElement)) {
284                 String name = e.getChildTextNormalize("name");
285                 try {
286                     MCRPath pChild = (MCRPath) mcrPath.resolve(name);
287                     Document doc = MCRPathXML.getDirectoryXML(pChild);
288                     Element eChildren = doc.getRootElement().getChild("children");
289                     if (eChildren != null) {
290                         e.addContent(eChildren.detach());
291                         createXMLForSubdirectories(pChild, e, currentDepth + 1, maxDepth);
292                     }
293                 } catch (IOException ex) {
294                     //ignore
295                 }
296 
297             }
298         }
299     }
300 
301     /**
302      * returns a list of objects
303      * @param info - the injected Jersey URIInfo object
304      *
305      * @param format - the output format ('xml'|'json')
306      * @param filter - a filter criteria
307      * @param sort - the sort criteria
308      *
309      * @return a Jersey response object
310      * @throws MCRRestAPIException    
311      * 
312      * @see MCRRestAPIObjects#listObjects(UriInfo, String, String, String)
313      * 
314      */
315     public static Response listObjects(UriInfo info, String format, String filter,
316         String sort) throws MCRRestAPIException {
317         List<MCRRestAPIError> errors = new ArrayList<>();
318         //analyze sort
319         MCRRestAPISortObject sortObj = null;
320         try {
321             sortObj = createSortObject(sort);
322         } catch (MCRRestAPIException rae) {
323             errors.addAll(rae.getErrors());
324         }
325 
326         //analyze format
327 
328         if (!format.equals(MCRRestAPIObjects.FORMAT_JSON) && !format.equals(MCRRestAPIObjects.FORMAT_XML)) {
329             errors.add(new MCRRestAPIError(MCRRestAPIError.CODE_WRONG_PARAMETER, "The parameter 'format' is wrong.",
330                 "Allowed values for format are 'json' or 'xml'."));
331         }
332 
333         //analyze filter
334         List<String> projectIDs = new ArrayList<>();
335         List<String> typeIDs = new ArrayList<>();
336         String lastModifiedBefore = null;
337         String lastModifiedAfter = null;
338         if (filter != null) {
339             for (String s : filter.split(";")) {
340                 if (s.startsWith("project:")) {
341                     projectIDs.add(s.substring(8));
342                     continue;
343                 }
344                 if (s.startsWith("type:")) {
345                     typeIDs.add(s.substring(5));
346                     continue;
347                 }
348                 if (s.startsWith("lastModifiedBefore:")) {
349                     if (!validateDateInput(s.substring(19))) {
350                         errors.add(new MCRRestAPIError(MCRRestAPIError.CODE_WRONG_PARAMETER,
351                             "The parameter 'filter' is wrong.",
352                             "The value of lastModifiedBefore could not be parsed. "
353                                 + "Please use UTC syntax: yyyy-MM-dd'T'HH:mm:ss'Z'."));
354                         continue;
355                     }
356                     if (lastModifiedBefore == null) {
357                         lastModifiedBefore = s.substring(19);
358                     } else if (s.substring(19).compareTo(lastModifiedBefore) < 0) {
359                         lastModifiedBefore = s.substring(19);
360                     }
361                     continue;
362                 }
363 
364                 if (s.startsWith("lastModifiedAfter:")) {
365                     if (!validateDateInput(s.substring(18))) {
366                         errors.add(new MCRRestAPIError(MCRRestAPIError.CODE_WRONG_PARAMETER,
367                             "The parameter 'filter' is wrong.",
368                             "The value of lastModifiedAfter could not be parsed. "
369                                 + "Please use UTC syntax: yyyy-MM-dd'T'HH:mm:ss'Z'."));
370                         continue;
371                     }
372                     if (lastModifiedAfter == null) {
373                         lastModifiedAfter = s.substring(18);
374                     } else if (s.substring(18).compareTo(lastModifiedAfter) > 0) {
375                         lastModifiedAfter = s.substring(18);
376                     }
377                     continue;
378                 }
379 
380                 errors.add(new MCRRestAPIError(MCRRestAPIError.CODE_WRONG_PARAMETER, "The parameter 'filter' is wrong.",
381                     "The syntax of the filter '" + s
382                         + "'could not be parsed. The syntax should be [filterName]:[value]. "
383                         + "Allowed filterNames are 'project', 'type', 'lastModifiedBefore' and 'lastModifiedAfter'."));
384             }
385         }
386 
387         if (errors.size() > 0) {
388             throw new MCRRestAPIException(Status.BAD_REQUEST, errors);
389         }
390         //Parameters are validated - continue to retrieve data
391 
392         //retrieve MCRIDs by Type and Project ID
393         Set<String> mcrIDs = new HashSet<>();
394         if (projectIDs.isEmpty()) {
395             if (typeIDs.isEmpty()) {
396                 mcrIDs = MCRXMLMetadataManager.instance().listIDs().stream().filter(id -> !id.contains("_derivate_"))
397                     .collect(Collectors.toSet());
398             } else {
399                 for (String t : typeIDs) {
400                     mcrIDs.addAll(MCRXMLMetadataManager.instance().listIDsOfType(t));
401                 }
402             }
403         } else {
404 
405             if (typeIDs.isEmpty()) {
406                 for (String id : MCRXMLMetadataManager.instance().listIDs()) {
407                     String[] split = id.split("_");
408                     if (!split[1].equals("derivate") && projectIDs.contains(split[0])) {
409                         mcrIDs.add(id);
410                     }
411                 }
412             } else {
413                 for (String p : projectIDs) {
414                     for (String t : typeIDs) {
415                         mcrIDs.addAll(MCRXMLMetadataManager.instance().listIDsForBase(p + "_" + t));
416                     }
417                 }
418             }
419         }
420 
421         //Filter by modifiedBefore and modifiedAfter
422         List<String> l = new ArrayList<>();
423         l.addAll(mcrIDs);
424         List<MCRObjectIDDate> objIdDates = new ArrayList<>();
425         try {
426             objIdDates = MCRXMLMetadataManager.instance().retrieveObjectDates(l);
427         } catch (IOException e) {
428             //TODO
429         }
430         if (lastModifiedAfter != null || lastModifiedBefore != null) {
431             List<MCRObjectIDDate> testObjIdDates = objIdDates;
432             objIdDates = new ArrayList<>();
433             for (MCRObjectIDDate oid : testObjIdDates) {
434                 String test = SDF_UTC.format(oid.getLastModified());
435                 if (lastModifiedAfter != null && test.compareTo(lastModifiedAfter) < 0) {
436                     continue;
437                 }
438                 if (lastModifiedBefore != null
439                     && lastModifiedBefore.compareTo(test.substring(0, lastModifiedBefore.length())) < 0) {
440                     continue;
441                 }
442                 objIdDates.add(oid);
443             }
444         }
445 
446         //sort if necessary
447         if (sortObj != null) {
448             objIdDates.sort(new MCRRestAPISortObjectComparator(sortObj));
449         }
450 
451         //output as XML
452         if (MCRRestAPIObjects.FORMAT_XML.equals(format)) {
453             Element eMcrobjects = new Element("mycoreobjects");
454             Document docOut = new Document(eMcrobjects);
455             eMcrobjects.setAttribute("numFound", Integer.toString(objIdDates.size()));
456             for (MCRObjectIDDate oid : objIdDates) {
457                 Element eMcrObject = new Element("mycoreobject");
458                 eMcrObject.setAttribute("ID", oid.getId());
459                 eMcrObject.setAttribute("lastModified", SDF_UTC.format(oid.getLastModified()));
460                 eMcrObject.setAttribute("href", info.getAbsolutePathBuilder().path(oid.getId()).build().toString());
461 
462                 eMcrobjects.addContent(eMcrObject);
463             }
464             try {
465                 StringWriter sw = new StringWriter();
466                 XMLOutputter xout = new XMLOutputter(Format.getPrettyFormat());
467                 xout.output(docOut, sw);
468                 return Response.ok(sw.toString())
469                     .type("application/xml; charset=UTF-8")
470                     .build();
471             } catch (IOException e) {
472                 throw new MCRRestAPIException(Response.Status.INTERNAL_SERVER_ERROR,
473                     new MCRRestAPIError(MCRRestAPIError.CODE_INTERNAL_ERROR, GENERAL_ERROR_MSG, e.getMessage()));
474             }
475         }
476 
477         //output as JSON
478         if (MCRRestAPIObjects.FORMAT_JSON.equals(format)) {
479             StringWriter sw = new StringWriter();
480             try {
481                 JsonWriter writer = new JsonWriter(sw);
482                 writer.setIndent("    ");
483                 writer.beginObject();
484                 writer.name("numFound").value(objIdDates.size());
485                 writer.name("mycoreobjects");
486                 writer.beginArray();
487                 for (MCRObjectIDDate oid : objIdDates) {
488                     writer.beginObject();
489                     writer.name("ID").value(oid.getId());
490                     writer.name("lastModified").value(SDF_UTC.format(oid.getLastModified()));
491                     writer.name("href").value(info.getAbsolutePathBuilder().path(oid.getId()).build().toString());
492                     writer.endObject();
493                 }
494                 writer.endArray();
495                 writer.endObject();
496 
497                 writer.close();
498                 return Response.ok(sw.toString())
499                     .type("application/json; charset=UTF-8")
500                     .build();
501             } catch (IOException e) {
502                 throw new MCRRestAPIException(Response.Status.INTERNAL_SERVER_ERROR,
503                     new MCRRestAPIError(MCRRestAPIError.CODE_INTERNAL_ERROR, GENERAL_ERROR_MSG, e.getMessage()));
504             }
505         }
506         throw new MCRRestAPIException(Response.Status.INTERNAL_SERVER_ERROR,
507             new MCRRestAPIError(MCRRestAPIError.CODE_INTERNAL_ERROR, "A problem in programm flow", null));
508     }
509 
510     /**
511      * returns a list of derivate objects
512      * @param info - the injected Jersey URIInfo object
513      *
514      * @param mcrObjID - the MyCoRe Object ID
515      * @param format - the output format ('xml'|'json')
516      * @param sort - the sort criteria
517      *
518      * @return a Jersey response object
519      * @throws MCRRestAPIException    
520      * 
521      * 
522      * @see MCRRestAPIObjects#listDerivates(UriInfo, String, String, String)
523      */
524     public static Response listDerivates(UriInfo info, String mcrObjID, String format,
525         String sort) throws MCRRestAPIException {
526         List<MCRRestAPIError> errors = new ArrayList<>();
527 
528         MCRRestAPISortObject sortObj = null;
529         try {
530             sortObj = createSortObject(sort);
531         } catch (MCRRestAPIException rae) {
532             errors.addAll(rae.getErrors());
533         }
534 
535         //analyze format
536 
537         if (!format.equals(MCRRestAPIObjects.FORMAT_JSON) && !format.equals(MCRRestAPIObjects.FORMAT_XML)) {
538             errors.add(new MCRRestAPIError(MCRRestAPIError.CODE_WRONG_PARAMETER, "The Parameter format is wrong.",
539                 "Allowed values for format are 'json' or 'xml'."));
540         }
541 
542         if (errors.size() > 0) {
543             throw new MCRRestAPIException(Status.BAD_REQUEST, errors);
544         }
545 
546         //Parameters are checked - continue to retrieve data
547 
548         List<MCRObjectIDDate> objIdDates = retrieveMCRObject(mcrObjID).getStructure().getDerivates().stream()
549             .map(MCRMetaLinkID::getXLinkHrefID).filter(MCRMetadataManager::exists).map(id -> new MCRObjectIDDate() {
550                 long lastModified;
551                 {
552                     try {
553                         lastModified = MCRXMLMetadataManager.instance().getLastModified(id);
554                     } catch (IOException e) {
555                         lastModified = 0;
556                         LOGGER.error(
557                             "Exception while getting last modified of {}",
558                             id, e);
559                     }
560                 }
561 
562                 @Override
563                 public String getId() {
564                     return id.toString();
565                 }
566 
567                 @Override
568                 public Date getLastModified() {
569                     return new Date(lastModified);
570                 }
571             }).sorted(new MCRRestAPISortObjectComparator(sortObj)::compare).collect(Collectors.toList());
572 
573         //output as XML
574         if (MCRRestAPIObjects.FORMAT_XML.equals(format)) {
575             Element eDerObjects = new Element("derobjects");
576             Document docOut = new Document(eDerObjects);
577             eDerObjects.setAttribute("numFound", Integer.toString(objIdDates.size()));
578             for (MCRObjectIDDate oid : objIdDates) {
579                 Element eDerObject = new Element("derobject");
580                 eDerObject.setAttribute("ID", oid.getId());
581                 MCRDerivate der = MCRMetadataManager.retrieveMCRDerivate(MCRObjectID.getInstance(oid.getId()));
582                 String mcrID = der.getDerivate().getMetaLink().getXLinkHref();
583                 eDerObject.setAttribute("metadata", mcrID);
584                 if (der.getDerivate().getClassifications().size() > 0) {
585                     StringBuffer c = new StringBuffer();
586                     for (int i = 0; i < der.getDerivate().getClassifications().size(); i++) {
587                         if (i > 0) {
588                             c.append(' ');
589                         }
590                         MCRMetaClassification cl = der.getDerivate().getClassifications().get(i);
591                         c.append(cl.getClassId()).append(':').append(cl.getCategId());
592                     }
593                     eDerObject.setAttribute("classifications", c.toString());
594                 }
595                 eDerObject.setAttribute("lastModified", SDF_UTC.format(oid.getLastModified()));
596                 eDerObject.setAttribute("href", info.getAbsolutePathBuilder().path(oid.getId()).build().toString());
597 
598                 eDerObjects.addContent(eDerObject);
599             }
600             try {
601                 StringWriter sw = new StringWriter();
602                 XMLOutputter xout = new XMLOutputter(Format.getPrettyFormat());
603                 xout.output(docOut, sw);
604                 return Response.ok(sw.toString())
605                     .type("application/xml; charset=UTF-8")
606                     .build();
607             } catch (IOException e) {
608                 throw new MCRRestAPIException(Response.Status.INTERNAL_SERVER_ERROR,
609                     new MCRRestAPIError(MCRRestAPIError.CODE_INTERNAL_ERROR, GENERAL_ERROR_MSG, e.getMessage()));
610             }
611         }
612 
613         //output as JSON
614         if (MCRRestAPIObjects.FORMAT_JSON.equals(format)) {
615             StringWriter sw = new StringWriter();
616             try {
617                 JsonWriter writer = new JsonWriter(sw);
618                 writer.setIndent("    ");
619                 writer.beginObject();
620                 writer.name("numFound").value(objIdDates.size());
621                 writer.name("mycoreobjects");
622                 writer.beginArray();
623                 for (MCRObjectIDDate oid : objIdDates) {
624                     writer.beginObject();
625                     writer.name("ID").value(oid.getId());
626                     MCRDerivate der = MCRMetadataManager.retrieveMCRDerivate(MCRObjectID.getInstance(oid.getId()));
627                     String mcrID = der.getDerivate().getMetaLink().getXLinkHref();
628                     writer.name("metadata").value(mcrID);
629                     writer.name("lastModified").value(SDF_UTC.format(oid.getLastModified()));
630                     writer.name("href").value(info.getAbsolutePathBuilder().path(oid.getId()).build().toString());
631                     writer.endObject();
632                 }
633                 writer.endArray();
634                 writer.endObject();
635 
636                 writer.close();
637 
638                 return Response.ok(sw.toString())
639                     .type("application/json; charset=UTF-8")
640                     .build();
641             } catch (IOException e) {
642                 throw new MCRRestAPIException(Response.Status.INTERNAL_SERVER_ERROR,
643                     new MCRRestAPIError(MCRRestAPIError.CODE_INTERNAL_ERROR, GENERAL_ERROR_MSG, e.getMessage()));
644             }
645         }
646 
647         throw new MCRRestAPIException(Response.Status.INTERNAL_SERVER_ERROR,
648             new MCRRestAPIError(MCRRestAPIError.CODE_INTERNAL_ERROR, "Unexepected program flow termination.",
649                 "Please contact a developer!"));
650     }
651 
652     /**
653      * lists derivate content (file listing)
654      * @param info - the Jersey UriInfo Object
655      * @param request - the HTTPServletRequest object
656      * @param mcrObjID - the MyCoRe Object ID
657      * @param mcrDerID - the MyCoRe Derivate ID
658      * @param format - the output format ('xml'|'json')
659      * @param path - the sub path of a directory inside the derivate
660      * @param depth - the level of subdirectories to be returned
661      * @return a Jersey Response object
662      * @throws MCRRestAPIException
663      */
664     public static Response listContents(UriInfo info, Application app, Request request, String mcrObjID,
665         String mcrDerID, String format, String path, int depth) throws MCRRestAPIException {
666 
667         if (!format.equals(MCRRestAPIObjects.FORMAT_JSON) && !format.equals(MCRRestAPIObjects.FORMAT_XML)) {
668             throw new MCRRestAPIException(Response.Status.BAD_REQUEST,
669                 new MCRRestAPIError(MCRRestAPIError.CODE_WRONG_PARAMETER, "The syntax of format parameter is wrong.",
670                     "Allowed values for format are 'json' or 'xml'."));
671         }
672         MCRObjectID mcrObj = MCRObjectID.getInstance(mcrObjID);
673         MCRDerivate derObj = retrieveMCRDerivate(mcrObj, mcrDerID);
674 
675         try {
676             MCRPath root = MCRPath.getPath(derObj.getId().toString(), "/");
677             BasicFileAttributes readAttributes = Files.readAttributes(root, BasicFileAttributes.class);
678             Date lastModified = new Date(readAttributes.lastModifiedTime().toMillis());
679             ResponseBuilder responseBuilder = request.evaluatePreconditions(lastModified);
680             if (responseBuilder != null) {
681                 return responseBuilder.build();
682             }
683             switch (format) {
684             case MCRRestAPIObjects.FORMAT_XML:
685                 Document docOut = listDerivateContentAsXML(derObj, path, depth, info, app);
686                 try (StringWriter sw = new StringWriter()) {
687                     XMLOutputter xout = new XMLOutputter(Format.getPrettyFormat());
688                     xout.output(docOut, sw);
689                     return response(sw.toString(), "application/xml", lastModified);
690                 } catch (IOException e) {
691                     throw new MCRRestAPIException(Response.Status.INTERNAL_SERVER_ERROR,
692                         new MCRRestAPIError(MCRRestAPIError.CODE_INTERNAL_ERROR, GENERAL_ERROR_MSG,
693                             e.getMessage()));
694                 }
695             case MCRRestAPIObjects.FORMAT_JSON:
696                 if (MCRRestAPIObjects.FORMAT_JSON.equals(format)) {
697                     String result = listDerivateContentAsJson(derObj, path, depth, info, app);
698                     return response(result, "application/json", lastModified);
699                 }
700             default:
701                 throw new MCRRestAPIException(Response.Status.INTERNAL_SERVER_ERROR,
702                     new MCRRestAPIError(MCRRestAPIError.CODE_INTERNAL_ERROR,
703                         "Unexepected program flow termination.",
704                         "Please contact a developer!"));
705             }
706         } catch (IOException e) {
707             throw new MCRRestAPIException(Response.Status.INTERNAL_SERVER_ERROR, new MCRRestAPIError(
708                 MCRRestAPIError.CODE_INTERNAL_ERROR, "Unexepected program flow termination.", e.getMessage()));
709         }
710     }
711 
712     /**
713      * returns the URL of the main document of a derivate
714      * 
715      * @param info - the Jersey UriInfo object
716      * @param mcrObjID - the MyCoRe Object ID
717      * @param mcrDerID - the MyCoRe Derivate ID
718      * 
719      * @return the Resolving URL for the main document of the derivate
720      * @throws IOException
721      */
722     public static String retrieveMaindocURL(UriInfo info, String mcrObjID, String mcrDerID, Application app)
723         throws IOException {
724         try {
725             MCRObjectID mcrObj = MCRObjectID.getInstance(mcrObjID);
726             MCRDerivate derObj = retrieveMCRDerivate(mcrObj, mcrDerID);
727             String maindoc = derObj.getDerivate().getInternals().getMainDoc();
728 
729             String baseURL = MCRJerseyUtil.getBaseURL(info, app)
730                 + MCRConfiguration2.getStringOrThrow("MCR.RestAPI.v1.Files.URL.path");
731             baseURL = baseURL.replace("${mcrid}", mcrObj.toString()).replace("${derid}",
732                 derObj.getId().toString());
733 
734             return baseURL + maindoc;
735         } catch (MCRRestAPIException rae) {
736             return null;
737         }
738     }
739 
740     private static Response response(String response, String type, Date lastModified) {
741         byte[] responseBytes = response.getBytes(StandardCharsets.UTF_8);
742         String mimeType = type + "; charset=UTF-8";
743         CacheControl cacheControl = new CacheControl();
744         cacheControl.setNoTransform(false);
745         cacheControl.setMaxAge(0);
746         return Response.ok(responseBytes, mimeType).lastModified(lastModified)
747             .header("Content-Length", responseBytes.length)
748             .cacheControl(cacheControl).build();
749     }
750 
751     /**
752      * validates the given String if it matches the UTC syntax or the beginning of it
753      * @param test
754      * @return true, if it is valid
755      */
756     private static boolean validateDateInput(String test) {
757         String base = "0000-00-00T00:00:00Z";
758         if (test.length() > base.length()) {
759             return false;
760         }
761         test = test + base.substring(test.length());
762         try {
763             SDF_UTC.parse(test);
764         } catch (ParseException e) {
765             return false;
766         }
767         return true;
768     }
769 
770     private static MCRRestAPISortObject createSortObject(String input) throws MCRRestAPIException {
771         if (input == null) {
772             return null;
773         }
774         List<MCRRestAPIError> errors = new ArrayList<>();
775         MCRRestAPISortObject result = new MCRRestAPISortObject();
776 
777         String[] data = input.split(":");
778         if (data.length == 2) {
779             result.setField(data[0].replace("|", ""));
780             String sortOrder = data[1].toLowerCase(Locale.GERMAN).replace("|", "");
781             if (!"ID".equals(result.getField()) && !"lastModified".equals(result.getField())) {
782                 errors.add(new MCRRestAPIError(MCRRestAPIError.CODE_WRONG_QUERY_PARAMETER, "The sortField is wrong",
783                     "Allowed values are 'ID' and 'lastModified'."));
784             }
785 
786             if ("asc".equals(sortOrder)) {
787                 result.setOrder(SortOrder.ASC);
788             }
789             if ("desc".equals(sortOrder)) {
790                 result.setOrder(SortOrder.DESC);
791             }
792             if (result.getOrder() == null) {
793                 errors.add(new MCRRestAPIError(MCRRestAPIError.CODE_WRONG_QUERY_PARAMETER, "The sortOrder is wrong",
794                     "Allowed values for sortOrder are 'asc' and 'desc'."));
795             }
796 
797         } else {
798             errors.add(new MCRRestAPIError(MCRRestAPIError.CODE_WRONG_QUERY_PARAMETER, "The sort parameter is wrong.",
799                 "The syntax should be [sortField]:[sortOrder]."));
800         }
801         if (errors.size() > 0) {
802             throw new MCRRestAPIException(Status.BAD_REQUEST, errors);
803         }
804         return result;
805     }
806 
807     private static MCRObject retrieveMCRObject(String idString) throws MCRRestAPIException {
808         String key = "mcr"; // the default value for the key
809         if (idString.contains(":")) {
810             int pos = idString.indexOf(":");
811             key = idString.substring(0, pos);
812             idString = idString.substring(pos + 1);
813             if (!key.equals("mcr")) {
814                 idString = URLDecoder.decode(idString, StandardCharsets.UTF_8);
815                 //ToDo - Shall we restrict the key set with a property?
816 
817                 //throw new MCRRestAPIException(MCRRestAPIError.create(Response.Status.BAD_REQUEST,
818                 //        "The ID is not valid.", "The prefix is unkown. Only 'mcr' is allowed."));
819             }
820         }
821         if (key.equals("mcr")) {
822 
823             MCRObjectID mcrID = null;
824             try {
825                 mcrID = MCRObjectID.getInstance(idString);
826             } catch (Exception e) {
827                 throw new MCRRestAPIException(Response.Status.BAD_REQUEST,
828                     new MCRRestAPIError(MCRRestAPIError.CODE_WRONG_ID,
829                         "The MyCoRe ID '" + idString
830                             + "' is not valid. - Did you use the proper format: '{project}_{type}_{number}'?",
831                         e.getMessage()));
832             }
833 
834             if (!MCRMetadataManager.exists(mcrID)) {
835                 throw new MCRRestAPIException(Response.Status.NOT_FOUND,
836                     new MCRRestAPIError(MCRRestAPIError.CODE_NOT_FOUND,
837                         "There is no object with the given MyCoRe ID '" + idString + "'.", null));
838             }
839 
840             return MCRMetadataManager.retrieveMCRObject(mcrID);
841         } else {
842             SolrClient solrClient = MCRSolrClientFactory.getMainSolrClient();
843             SolrQuery query = new SolrQuery();
844             query.setQuery(key + ":" + idString);
845             try {
846                 QueryResponse response = solrClient.query(query);
847                 SolrDocumentList solrResults = response.getResults();
848                 if (solrResults.getNumFound() == 1) {
849                     String id = solrResults.get(0).getFieldValue("returnId").toString();
850                     return retrieveMCRObject(id);
851                 } else {
852                     if (solrResults.getNumFound() == 0) {
853                         throw new MCRRestAPIException(Response.Status.NOT_FOUND,
854                             new MCRRestAPIError(MCRRestAPIError.CODE_NOT_FOUND,
855                                 "There is no object with the given ID '" + key + ":" + idString + "'.", null));
856                     } else {
857                         throw new MCRRestAPIException(Response.Status.NOT_FOUND,
858                             new MCRRestAPIError(MCRRestAPIError.CODE_NOT_FOUND,
859                                 "The ID is not unique. There are " + solrResults.getNumFound()
860                                     + " objecst fore the given ID '" + key + ":" + idString + "'.",
861                                 null));
862                     }
863                 }
864             } catch (SolrServerException | IOException e) {
865                 LOGGER.error(e);
866                 throw new MCRRestAPIException(Response.Status.BAD_REQUEST,
867                     new MCRRestAPIError(MCRRestAPIError.CODE_INTERNAL_ERROR, "Internal server error.", e.getMessage()));
868             }
869         }
870     }
871 
872     private static MCRDerivate retrieveMCRDerivate(MCRObjectID parentObjId, String derIDString)
873         throws MCRRestAPIException {
874 
875         String derKey = "mcr"; // the default value for the key
876         if (derIDString.contains(":")) {
877             int pos = derIDString.indexOf(":");
878             derKey = derIDString.substring(0, pos);
879             derIDString = derIDString.substring(pos + 1);
880             if (!derKey.equals("mcr") && !derKey.equals("label")) {
881                 throw new MCRRestAPIException(Response.Status.BAD_REQUEST,
882                     new MCRRestAPIError(MCRRestAPIError.CODE_WRONG_ID, "The ID is not valid.",
883                         "The prefix is unkown. Only 'mcr' or 'label' are allowed."));
884             }
885         }
886 
887         String matchedDerID = null;
888         if ("mcr".equals(derKey) &&
889             MCRMetadataManager.getDerivateIds(parentObjId, 0, TimeUnit.SECONDS)
890                 .stream()
891                 .map(MCRObjectID::toString)
892                 .anyMatch(derIDString::equals)) {
893             matchedDerID = derIDString;
894         }
895         if (derKey.equals("label")) {
896             MCRObject mcrObj = MCRMetadataManager.retrieveMCRObject(parentObjId);
897             for (MCRMetaLinkID check : mcrObj.getStructure().getDerivates()) {
898                 if (derIDString.equals(check.getXLinkLabel()) || derIDString.equals(check.getXLinkTitle())) {
899                     matchedDerID = check.getXLinkHref();
900                     break;
901                 }
902             }
903         }
904 
905         if (matchedDerID == null) {
906             throw new MCRRestAPIException(Response.Status.NOT_FOUND,
907                 new MCRRestAPIError(MCRRestAPIError.CODE_NOT_FOUND, "Derivate " + derIDString + " not found.",
908                     "The MyCoRe Object with id '" + parentObjId
909                         + "' does not contain a derivate with id '" + derIDString + "'."));
910         }
911 
912         MCRObjectID derID = MCRObjectID.getInstance(matchedDerID);
913         if (!MCRMetadataManager.exists(derID)) {
914             throw new MCRRestAPIException(Response.Status.NOT_FOUND, new MCRRestAPIError(MCRRestAPIError.CODE_NOT_FOUND,
915                 "There is no derivate with the id '" + matchedDerID + "'.", null));
916         }
917         return MCRMetadataManager.retrieveMCRDerivate(derID);
918     }
919 
920     /**
921      * checks if the given path is a directory and contains children
922      * 
923      * @param p - the path to check
924      * @return true, if there are children
925      */
926 
927     public static boolean hasChildren(Path p) {
928         try {
929             if (Files.isDirectory(p)) {
930                 try (DirectoryStream<Path> ds = Files.newDirectoryStream(p)) {
931                     return ds.iterator().hasNext();
932                 }
933             }
934         } catch (IOException e) {
935             LOGGER.error(e);
936         }
937         return false;
938     }
939 }