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;
20  
21  import java.io.IOException;
22  import java.io.StringWriter;
23  import java.util.Date;
24  import java.util.concurrent.TimeUnit;
25  
26  import org.apache.logging.log4j.LogManager;
27  import org.apache.logging.log4j.Logger;
28  import org.apache.solr.client.solrj.SolrClient;
29  import org.apache.solr.client.solrj.SolrQuery;
30  import org.apache.solr.client.solrj.SolrServerException;
31  import org.apache.solr.client.solrj.response.QueryResponse;
32  import org.apache.solr.common.SolrDocumentList;
33  import org.glassfish.jersey.server.ContainerRequest;
34  import org.jdom2.Document;
35  import org.jdom2.Element;
36  import org.jdom2.Namespace;
37  import org.jdom2.filter.Filters;
38  import org.jdom2.output.Format;
39  import org.jdom2.output.XMLOutputter;
40  import org.jdom2.xpath.XPathExpression;
41  import org.jdom2.xpath.XPathFactory;
42  import org.mycore.datamodel.classifications2.MCRCategory;
43  import org.mycore.datamodel.classifications2.MCRCategoryDAO;
44  import org.mycore.datamodel.classifications2.MCRCategoryID;
45  import org.mycore.datamodel.classifications2.impl.MCRCategoryDAOImpl;
46  import org.mycore.datamodel.classifications2.utils.MCRCategoryTransformer;
47  import org.mycore.frontend.jersey.MCRCacheControl;
48  import org.mycore.frontend.jersey.MCRJerseyUtil;
49  import org.mycore.restapi.v1.errors.MCRRestAPIError;
50  import org.mycore.restapi.v1.errors.MCRRestAPIException;
51  import org.mycore.solr.MCRSolrClientFactory;
52  import org.mycore.solr.MCRSolrUtils;
53  
54  import com.google.gson.stream.JsonWriter;
55  
56  import jakarta.ws.rs.DefaultValue;
57  import jakarta.ws.rs.GET;
58  import jakarta.ws.rs.Path;
59  import jakarta.ws.rs.PathParam;
60  import jakarta.ws.rs.Produces;
61  import jakarta.ws.rs.QueryParam;
62  import jakarta.ws.rs.core.Context;
63  import jakarta.ws.rs.core.MediaType;
64  import jakarta.ws.rs.core.Response;
65  import jakarta.ws.rs.core.UriInfo;
66  import jakarta.ws.rs.core.Response.Status;
67  
68  /**
69   * REST API for classification objects.
70   *
71   * @author Robert Stephan
72   *
73   * @version $Revision: $ $Date: $
74   */
75  @Path("/classifications")
76  public class MCRRestAPIClassifications {
77  
78      public static final String FORMAT_JSON = "json";
79  
80      public static final String FORMAT_XML = "xml";
81  
82      private static final MCRCategoryDAO DAO = new MCRCategoryDAOImpl();
83  
84      private static Logger LOGGER = LogManager.getLogger(MCRRestAPIClassifications.class);
85  
86      @Context
87      ContainerRequest request;
88  
89      private Date lastModified = new Date(DAO.getLastModified());
90  
91      /**
92       * Output xml
93       * @param eRoot - the root element
94       * @param lang - the language which should be filtered or null for no filter
95       * @return a string representation of the XML
96       * @throws IOException
97       */
98      private static String writeXML(Element eRoot, String lang) throws IOException {
99          StringWriter sw = new StringWriter();
100         if (lang != null) {
101             // <label xml:lang="en" text="part" />
102             XPathExpression<Element> xpE = XPathFactory.instance().compile("//label[@xml:lang!='" + lang + "']",
103                 Filters.element(), null, Namespace.XML_NAMESPACE);
104             for (Element e : xpE.evaluate(eRoot)) {
105                 e.getParentElement().removeContent(e);
106             }
107         }
108         XMLOutputter xout = new XMLOutputter(Format.getPrettyFormat());
109         Document docOut = new Document(eRoot.detach());
110         xout.output(docOut, sw);
111         return sw.toString();
112     }
113 
114     /**
115      * output categories in JSON format
116      * @param eParent - the parent xml element
117      * @param writer - the JSON writer
118      * @param lang - the language to be filtered or null if all languages should be displayed
119      *
120      * @throws IOException
121      */
122     private static void writeChildrenAsJSON(Element eParent, JsonWriter writer, String lang) throws IOException {
123         if (eParent.getChildren("category").size() == 0) {
124             return;
125         }
126 
127         writer.name("categories");
128         writer.beginArray();
129         for (Element e : eParent.getChildren("category")) {
130             writer.beginObject();
131             writer.name("ID").value(e.getAttributeValue("ID"));
132             writer.name("labels").beginArray();
133             for (Element eLabel : e.getChildren("label")) {
134                 if (lang == null || lang.equals(eLabel.getAttributeValue("lang", Namespace.XML_NAMESPACE))) {
135                     writer.beginObject();
136                     writer.name("lang").value(eLabel.getAttributeValue("lang", Namespace.XML_NAMESPACE));
137                     writer.name("text").value(eLabel.getAttributeValue("text"));
138                     if (eLabel.getAttributeValue("description") != null) {
139                         writer.name("description").value(eLabel.getAttributeValue("description"));
140                     }
141                     writer.endObject();
142                 }
143             }
144             writer.endArray();
145 
146             if (e.getChildren("category").size() > 0) {
147                 writeChildrenAsJSON(e, writer, lang);
148             }
149             writer.endObject();
150         }
151         writer.endArray();
152     }
153 
154     /**
155      * output children in JSON format used as input for Dijit Checkbox Tree
156      *
157      * @param eParent - the parent xml element
158      * @param writer - the JSON writer
159      * @param lang - the language to be filtered or null if all languages should be displayed
160      *
161      * @throws IOException
162      */
163     private static void writeChildrenAsJSONCBTree(Element eParent, JsonWriter writer, String lang, boolean checked)
164         throws IOException {
165         writer.beginArray();
166         for (Element e : eParent.getChildren("category")) {
167             writer.beginObject();
168             writer.name("ID").value(e.getAttributeValue("ID"));
169             for (Element eLabel : e.getChildren("label")) {
170                 if (lang == null || lang.equals(eLabel.getAttributeValue("lang", Namespace.XML_NAMESPACE))) {
171                     writer.name("text").value(eLabel.getAttributeValue("text"));
172                 }
173             }
174             writer.name("checked").value(checked);
175             if (e.getChildren("category").size() > 0) {
176                 writer.name("children");
177                 writeChildrenAsJSONCBTree(e, writer, lang, checked);
178             }
179             writer.endObject();
180         }
181         writer.endArray();
182     }
183 
184     /**
185      * output children in JSON format used as input for a jsTree
186      *
187      * @param eParent - the parent xml element
188      * @param writer - the JSON writer
189      * @param lang - the language to be filtered or null if all languages should be displayed
190      * @param opened - true, if all leaf nodes should be displayed
191      * @param disabled - true, if all nodes should be disabled
192      * @param selected - true, if all node should be selected
193      *
194      * @throws IOException
195      */
196     private static void writeChildrenAsJSONJSTree(Element eParent, JsonWriter writer, String lang, boolean opened,
197         boolean disabled, boolean selected) throws IOException {
198         writer.beginArray();
199         for (Element e : eParent.getChildren("category")) {
200             writer.beginObject();
201             writer.name("id").value(e.getAttributeValue("ID"));
202             for (Element eLabel : e.getChildren("label")) {
203                 if (lang == null || lang.equals(eLabel.getAttributeValue("lang", Namespace.XML_NAMESPACE))) {
204                     writer.name("text").value(eLabel.getAttributeValue("text"));
205                 }
206             }
207             if (opened || disabled || selected) {
208                 writer.name("state");
209                 writer.beginObject();
210                 if (opened) {
211                     writer.name("opened").value(true);
212                 }
213                 if (disabled) {
214                     writer.name("disabled").value(true);
215                 }
216                 if (selected) {
217                     writer.name("selected").value(true);
218                 }
219                 writer.endObject();
220             }
221             if (e.getChildren("category").size() > 0) {
222                 writer.name("children");
223                 writeChildrenAsJSONJSTree(e, writer, lang, opened, disabled, selected);
224             }
225             writer.endObject();
226         }
227         writer.endArray();
228     }
229 
230     /**
231      * lists all available classifications as XML or JSON
232      *
233      * @param info - the URIInfo object
234      * @param format - the output format ('xml' or 'json)
235      * @return a Jersey Response Object
236      */
237     @GET
238     @Produces({ MediaType.TEXT_XML + ";charset=UTF-8", MediaType.APPLICATION_JSON + ";charset=UTF-8" })
239     @MCRCacheControl(maxAge = @MCRCacheControl.Age(time = 1, unit = TimeUnit.HOURS),
240         sMaxAge = @MCRCacheControl.Age(time = 1, unit = TimeUnit.HOURS))
241     public Response listClassifications(@Context UriInfo info,
242         @QueryParam("format") @DefaultValue("json") String format) {
243 
244         Response.ResponseBuilder builder = request.evaluatePreconditions(lastModified);
245         if (builder != null) {
246             return builder.build();
247         }
248 
249         if (FORMAT_XML.equals(format)) {
250             StringWriter sw = new StringWriter();
251 
252             XMLOutputter xout = new XMLOutputter(Format.getPrettyFormat());
253             Document docOut = new Document();
254             Element eRoot = new Element("mycoreclassifications");
255             docOut.setRootElement(eRoot);
256 
257             for (MCRCategory cat : DAO.getRootCategories()) {
258                 eRoot.addContent(new Element("mycoreclass").setAttribute("ID", cat.getId().getRootID()).setAttribute(
259                     "href", info.getAbsolutePathBuilder().path(cat.getId().getRootID()).build().toString()));
260             }
261             try {
262                 xout.output(docOut, sw);
263                 return Response.ok(sw.toString())
264                     .lastModified(lastModified)
265                     .type("application/xml; charset=UTF-8")
266                     .build();
267             } catch (IOException e) {
268                 //ToDo
269             }
270         }
271 
272         if (FORMAT_JSON.equals(format)) {
273             StringWriter sw = new StringWriter();
274             try {
275                 JsonWriter writer = new JsonWriter(sw);
276                 writer.setIndent("    ");
277                 writer.beginObject();
278                 writer.name("mycoreclass");
279                 writer.beginArray();
280                 for (MCRCategory cat : DAO.getRootCategories()) {
281                     writer.beginObject();
282                     writer.name("ID").value(cat.getId().getRootID());
283                     writer.name("href")
284                         .value(info.getAbsolutePathBuilder().path(cat.getId().getRootID()).build().toString());
285                     writer.endObject();
286                 }
287                 writer.endArray();
288                 writer.endObject();
289 
290                 writer.close();
291 
292                 return Response.ok(sw.toString())
293                     .type(MCRJerseyUtil.APPLICATION_JSON_UTF8)
294                     .lastModified(lastModified)
295                     .build();
296             } catch (IOException e) {
297                 //toDo
298             }
299         }
300         return Response.status(Status.BAD_REQUEST).build();
301     }
302 
303     /**
304      *  returns a single classification object
305      *
306      * @param classID - the classfication id
307      * @param format
308      *   Possible values are: json | xml (required)
309      * @param filter
310      * 	 a ';'-separated list of ':'-separated key-value pairs, possible keys are:
311      *      - lang - the language of the returned labels, if ommited all labels in all languages will be returned
312      *      - root - an id for a category which will be used as root
313      *      - nonempty - hide empty categories
314      * @param style
315      * 	a ';'-separated list of values, possible keys are:
316      *   	- 'checkboxtree' - create a json syntax which can be used as input for a dojo checkboxtree;
317      *      - 'checked'   - (together with 'checkboxtree') all checkboxed will be checked
318      *      - 'jstree' - create a json syntax which can be used as input for a jsTree
319      *      - 'opened' - (together with 'jstree') - all nodes will be opened
320      *      - 'disabled' - (together with 'jstree') - all nodes will be disabled
321      *      - 'selected' - (together with 'jstree') - all nodes will be selected
322      * @param callback - used in JSONP to wrap json result into a Javascript function named by callback parameter
323      * @return a Jersey Response object
324      */
325     @GET
326     //@Path("/id/{value}{format:(\\.[^/]+?)?}")  -> working, but returns empty string instead of default value
327     @Path("/{classID}")
328     @Produces({ MediaType.TEXT_XML + ";charset=UTF-8", MediaType.APPLICATION_JSON + ";charset=UTF-8" })
329     @MCRCacheControl(maxAge = @MCRCacheControl.Age(time = 1, unit = TimeUnit.HOURS),
330         sMaxAge = @MCRCacheControl.Age(time = 1, unit = TimeUnit.HOURS))
331     public Response showObject(@PathParam("classID") String classID,
332         @QueryParam("format") @DefaultValue("xml") String format, @QueryParam("filter") @DefaultValue("") String filter,
333         @QueryParam("style") @DefaultValue("") String style,
334         @QueryParam("callback") @DefaultValue("") String callback) {
335 
336         Response.ResponseBuilder builder = request.evaluatePreconditions(lastModified);
337         if (builder != null) {
338             return builder.build();
339         }
340 
341         String rootCateg = null;
342         String lang = null;
343         boolean filterNonEmpty = false;
344         boolean filterNoChildren = false;
345 
346         for (String f : filter.split(";")) {
347             if (f.startsWith("root:")) {
348                 rootCateg = f.substring(5);
349             }
350             if (f.startsWith("lang:")) {
351                 lang = f.substring(5);
352             }
353             if (f.startsWith("nonempty")) {
354                 filterNonEmpty = true;
355             }
356             if (f.startsWith("nochildren")) {
357                 filterNoChildren = true;
358             }
359         }
360 
361         if (format == null || classID == null) {
362             return Response.serverError().status(Status.BAD_REQUEST).build();
363             //TODO response.sendError(HttpServletResponse.SC_NOT_FOUND,
364             //        "Please specify parameters format and classid.");
365         }
366         try {
367             MCRCategory cl = DAO.getCategory(MCRCategoryID.rootID(classID), -1);
368             if (cl == null) {
369                 throw new MCRRestAPIException(Status.NOT_FOUND,
370                     new MCRRestAPIError(MCRRestAPIError.CODE_NOT_FOUND, "Classification not found.",
371                         "There is no classification with the given ID."));
372             }
373             Document docClass = MCRCategoryTransformer.getMetaDataDocument(cl, false);
374             Element eRoot = docClass.getRootElement();
375             if (rootCateg != null) {
376                 XPathExpression<Element> xpe = XPathFactory.instance().compile("//category[@ID='" + rootCateg + "']",
377                     Filters.element());
378                 Element e = xpe.evaluateFirst(docClass);
379                 if (e != null) {
380                     eRoot = e;
381                 } else {
382                     throw new MCRRestAPIException(Status.NOT_FOUND,
383                         new MCRRestAPIError(MCRRestAPIError.CODE_NOT_FOUND, "Category not found.",
384                             "The classfication does not contain a category with the given ID."));
385                 }
386             }
387             if (filterNonEmpty) {
388                 Element eFilter = eRoot;
389                 if (eFilter.getName().equals("mycoreclass")) {
390                     eFilter = eFilter.getChild("categories");
391                 }
392                 filterNonEmpty(docClass.getRootElement().getAttributeValue("ID"), eFilter);
393             }
394             if (filterNoChildren) {
395                 eRoot.removeChildren("category");
396             }
397 
398             if (FORMAT_JSON.equals(format)) {
399                 String json = writeJSON(eRoot, lang, style);
400                 //eventually: allow Cross Site Requests: .header("Access-Control-Allow-Origin", "*")
401                 if (callback.length() > 0) {
402                     return Response.ok(callback + "(" + json + ")")
403                         .lastModified(lastModified)
404                         .type(MCRJerseyUtil.APPLICATION_JSON_UTF8)
405                         .build();
406                 } else {
407                     return Response.ok(json).type("application/json; charset=UTF-8")
408                         .build();
409                 }
410             }
411 
412             if (FORMAT_XML.equals(format)) {
413                 String xml = writeXML(eRoot, lang);
414                 return Response.ok(xml)
415                     .lastModified(lastModified)
416                     .type(MCRJerseyUtil.APPLICATION_XML_UTF8)
417                     .build();
418             }
419         } catch (Exception e) {
420             LogManager.getLogger(this.getClass()).error("Error outputting classification", e);
421             //TODO response.sendError(HttpServletResponse.SC_NOT_FOUND, "Error outputting classification");
422         }
423         return null;
424     }
425 
426     /**
427      * Output JSON
428      * @param eRoot - the category element
429      * @param lang - the language to be filtered for or null if all languages should be displayed
430      * @param style - the style
431      * @return a string representation of a JSON object
432      * @throws IOException
433      */
434     private String writeJSON(Element eRoot, String lang, String style) throws IOException {
435         StringWriter sw = new StringWriter();
436         JsonWriter writer = new JsonWriter(sw);
437         writer.setIndent("  ");
438         if (style.contains("checkboxtree")) {
439             if (lang == null) {
440                 lang = "de";
441             }
442             writer.beginObject();
443             writer.name("identifier").value(eRoot.getAttributeValue("ID"));
444             for (Element eLabel : eRoot.getChildren("label")) {
445                 if (lang.equals(eLabel.getAttributeValue("lang", Namespace.XML_NAMESPACE))) {
446                     writer.name("label").value(eLabel.getAttributeValue("text"));
447                 }
448             }
449             writer.name("items");
450             eRoot = eRoot.getChild("categories");
451             writeChildrenAsJSONCBTree(eRoot, writer, lang, style.contains("checked"));
452             writer.endObject();
453         } else if (style.contains("jstree")) {
454             if (lang == null) {
455                 lang = "de";
456             }
457             eRoot = eRoot.getChild("categories");
458             writeChildrenAsJSONJSTree(eRoot, writer, lang, style.contains("opened"),
459                 style.contains("disabled"), style.contains("selected"));
460         } else {
461             writer.beginObject(); // {
462             writer.name("ID").value(eRoot.getAttributeValue("ID"));
463             writer.name("label");
464             writer.beginArray();
465             for (Element eLabel : eRoot.getChildren("label")) {
466                 if (lang == null || lang.equals(eLabel.getAttributeValue("lang", Namespace.XML_NAMESPACE))) {
467                     writer.beginObject();
468                     writer.name("lang").value(eLabel.getAttributeValue("lang", Namespace.XML_NAMESPACE));
469                     writer.name("text").value(eLabel.getAttributeValue("text"));
470                     if (eLabel.getAttributeValue("description") != null) {
471                         writer.name("description").value(eLabel.getAttributeValue("description"));
472                     }
473                     writer.endObject();
474                 }
475             }
476             writer.endArray();
477 
478             if (eRoot.equals(eRoot.getDocument().getRootElement())) {
479                 writeChildrenAsJSON(eRoot.getChild("categories"), writer, lang);
480             } else {
481                 writeChildrenAsJSON(eRoot, writer, lang);
482             }
483 
484             writer.endObject();
485         }
486         writer.close();
487         return sw.toString();
488     }
489 
490     private void filterNonEmpty(String classId, Element e) {
491         SolrClient solrClient = MCRSolrClientFactory.getMainSolrClient();
492         Element[] categories = e.getChildren("category").toArray(Element[]::new);
493         for (Element cat : categories) {
494             SolrQuery solrQquery = new SolrQuery();
495             solrQquery.setQuery(
496                 "category:\"" + MCRSolrUtils.escapeSearchValue(classId + ":" + cat.getAttributeValue("ID")) + "\"");
497             solrQquery.setRows(0);
498             try {
499                 QueryResponse response = solrClient.query(solrQquery);
500                 SolrDocumentList solrResults = response.getResults();
501                 if (solrResults.getNumFound() == 0) {
502                     cat.detach();
503                 } else {
504                     filterNonEmpty(classId, cat);
505                 }
506             } catch (SolrServerException | IOException exc) {
507                 LOGGER.error(exc);
508             }
509 
510         }
511     }
512 
513 }