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.classeditor.resources;
20  
21  import static org.mycore.access.MCRAccessManager.PERMISSION_DELETE;
22  import static org.mycore.access.MCRAccessManager.PERMISSION_WRITE;
23  
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.text.MessageFormat;
27  import java.util.ArrayList;
28  import java.util.Collection;
29  import java.util.Comparator;
30  import java.util.LinkedList;
31  import java.util.List;
32  import java.util.Locale;
33  import java.util.Map;
34  import java.util.UUID;
35  
36  import org.apache.solr.client.solrj.SolrClient;
37  import org.apache.solr.client.solrj.SolrServerException;
38  import org.apache.solr.client.solrj.response.QueryResponse;
39  import org.apache.solr.common.SolrDocument;
40  import org.apache.solr.common.SolrDocumentList;
41  import org.apache.solr.common.params.ModifiableSolrParams;
42  import org.glassfish.jersey.media.multipart.FormDataParam;
43  import org.mycore.access.MCRAccessException;
44  import org.mycore.access.MCRAccessManager;
45  import org.mycore.common.MCRJSONManager;
46  import org.mycore.common.config.MCRConfiguration2;
47  import org.mycore.datamodel.classifications2.MCRCategLinkService;
48  import org.mycore.datamodel.classifications2.MCRCategLinkServiceFactory;
49  import org.mycore.datamodel.classifications2.MCRCategory;
50  import org.mycore.datamodel.classifications2.MCRCategoryDAO;
51  import org.mycore.datamodel.classifications2.MCRCategoryDAOFactory;
52  import org.mycore.datamodel.classifications2.MCRCategoryID;
53  import org.mycore.datamodel.classifications2.utils.MCRClassificationUtils;
54  import org.mycore.frontend.classeditor.access.MCRClassificationWritePermission;
55  import org.mycore.frontend.classeditor.access.MCRNewClassificationPermission;
56  import org.mycore.frontend.classeditor.json.MCRJSONCategory;
57  import org.mycore.frontend.classeditor.json.MCRJSONCategoryHelper;
58  import org.mycore.frontend.classeditor.wrapper.MCRCategoryListWrapper;
59  import org.mycore.frontend.jersey.filter.access.MCRRestrictedAccess;
60  import org.mycore.solr.MCRSolrClientFactory;
61  import org.mycore.solr.classification.MCRSolrClassificationUtil;
62  import org.mycore.solr.search.MCRSolrSearchUtils;
63  
64  import com.google.gson.Gson;
65  import com.google.gson.JsonArray;
66  import com.google.gson.JsonElement;
67  import com.google.gson.JsonObject;
68  import com.google.gson.JsonPrimitive;
69  import com.google.gson.JsonStreamParser;
70  
71  import jakarta.ws.rs.Consumes;
72  import jakarta.ws.rs.DELETE;
73  import jakarta.ws.rs.GET;
74  import jakarta.ws.rs.POST;
75  import jakarta.ws.rs.Path;
76  import jakarta.ws.rs.PathParam;
77  import jakarta.ws.rs.Produces;
78  import jakarta.ws.rs.QueryParam;
79  import jakarta.ws.rs.WebApplicationException;
80  import jakarta.ws.rs.core.Context;
81  import jakarta.ws.rs.core.MediaType;
82  import jakarta.ws.rs.core.Response;
83  import jakarta.ws.rs.core.UriInfo;
84  import jakarta.ws.rs.core.Response.Status;
85  
86  /**
87   * This class is responsible for CRUD-operations of MCRCategories. It accepts
88   * JSON objects of the form <code>
89   * [{    "ID":{"rootID":"abcd","categID":"1234"}
90   *      "label":[
91   *          {"lang":"de","text":"Rubriken Test 2 fuer MyCoRe","descriptions":"test de"},
92   *          {"lang":"en","text":"Rubric test 2 for MyCoRe","descriptions":"test en"}
93   *      ],
94   *      "parentID":{"rootID":"abcd","categID":"parent"}
95   *      "children:"URL"
96   *
97   * }
98   * ...
99   * ]
100  * </code>
101  *
102  * @author chi
103  *
104  */
105 @Path("classifications")
106 public class MCRClassificationEditorResource {
107     private static final MCRCategoryDAO CATEGORY_DAO = MCRCategoryDAOFactory.getInstance();
108 
109     private static final MCRCategLinkService CATEG_LINK_SERVICE = MCRCategLinkServiceFactory.getInstance();
110 
111     @Context
112     UriInfo uriInfo;
113 
114     @GET
115     @Produces(MediaType.TEXT_PLAIN)
116     @Path("test")
117     public String test() {
118         return "Hallo";
119     }
120 
121     /**
122      * @param rootidStr
123      *            rootID.categID
124      * @return
125      */
126     @GET
127     @Path("{rootidStr}")
128     @Produces(MediaType.APPLICATION_JSON)
129     public String get(@PathParam("rootidStr") String rootidStr) {
130         if (rootidStr == null || "".equals(rootidStr)) {
131             throw new WebApplicationException(Status.NOT_FOUND);
132         }
133 
134         MCRCategoryID id = MCRCategoryID.rootID(rootidStr);
135         return getCategory(id);
136     }
137 
138     /**
139      * @param rootidStr
140      *            rootID.categID
141      * @return
142      */
143     @GET
144     @Path("{rootidStr}/{categidStr}")
145     @Produces(MediaType.APPLICATION_JSON)
146     public String get(@PathParam("rootidStr") String rootidStr, @PathParam("categidStr") String categidStr) {
147 
148         if (rootidStr == null || "".equals(rootidStr) || categidStr == null || "".equals(categidStr)) {
149             throw new WebApplicationException(Status.NOT_FOUND);
150         }
151 
152         MCRCategoryID id = new MCRCategoryID(rootidStr, categidStr);
153         return getCategory(id);
154     }
155 
156     @GET
157     @Path("newID/{rootID}")
158     @Produces(MediaType.APPLICATION_JSON)
159     public String newIDJson(@PathParam("rootID") String rootID) {
160         Gson gson = MCRJSONManager.instance().createGson();
161         return gson.toJson(newRandomUUID(rootID));
162     }
163 
164     @GET
165     @Path("newID")
166     @MCRRestrictedAccess(MCRNewClassificationPermission.class)
167     @Produces(MediaType.APPLICATION_JSON)
168     public String newRootIDJson() {
169         Gson gson = MCRJSONManager.instance().createGson();
170         return gson.toJson(newRootID());
171     }
172 
173     @GET
174     @Path("export/{rootidStr}")
175     @Produces(MediaType.APPLICATION_XML)
176     public String export(@PathParam("rootidStr") String rootidStr) {
177         if (rootidStr == null || "".equals(rootidStr)) {
178             throw new WebApplicationException(Status.NOT_FOUND);
179         }
180         String classAsString = MCRClassificationUtils.asString(rootidStr);
181         if (classAsString == null) {
182             throw new WebApplicationException(Status.NOT_FOUND);
183         }
184         return classAsString;
185     }
186 
187     @GET
188     @Produces(MediaType.APPLICATION_JSON)
189     public Response getClassification() {
190         Gson gson = MCRJSONManager.instance().createGson();
191         List<MCRCategory> rootCategories = new LinkedList<>(CATEGORY_DAO.getRootCategories());
192         rootCategories.removeIf(
193             category -> !MCRAccessManager.checkPermission(category.getId().getRootID(), PERMISSION_WRITE));
194         if (rootCategories.isEmpty()
195             && !MCRAccessManager.checkPermission(MCRClassificationUtils.CREATE_CLASS_PERMISSION)) {
196             return Response.status(Status.UNAUTHORIZED).build();
197         }
198         Map<MCRCategoryID, Boolean> linkMap = CATEG_LINK_SERVICE.hasLinks(null);
199         String json = gson.toJson(new MCRCategoryListWrapper(rootCategories, linkMap));
200         return Response.ok(json).build();
201     }
202 
203     @DELETE
204     @Consumes(MediaType.APPLICATION_JSON)
205     public Response deleteCateg(String json) {
206         MCRJSONCategory category = parseJson(json);
207         DeleteOp deleteOp = new DeleteOp(category);
208         deleteOp.run();
209         return deleteOp.getResponse();
210     }
211 
212     @POST
213     @Path("save")
214     @MCRRestrictedAccess(MCRClassificationWritePermission.class)
215     @Consumes(MediaType.APPLICATION_JSON)
216     public Response save(String json) {
217         JsonStreamParser jsonStreamParser = new JsonStreamParser(json);
218         if (jsonStreamParser.hasNext()) {
219             JsonArray saveObjArray = jsonStreamParser.next().getAsJsonArray();
220             List<JsonObject> saveList = new ArrayList<>();
221             for (JsonElement jsonElement : saveObjArray) {
222                 saveList.add(jsonElement.getAsJsonObject());
223             }
224             saveList.sort(new IndexComperator());
225             for (JsonObject jsonObject : saveList) {
226                 String status = getStatus(jsonObject);
227                 SaveElement categ = getCateg(jsonObject);
228                 MCRJSONCategory parsedCateg = parseJson(categ.getJson());
229                 if ("update".equals(status)) {
230                     new UpdateOp(parsedCateg, jsonObject).run();
231                 } else if ("delete".equals(status)) {
232                     deleteCateg(categ.getJson());
233                 } else {
234                     return Response.status(Status.BAD_REQUEST).build();
235                 }
236             }
237             //            Status.CONFLICT
238             return Response.status(Status.OK).build();
239         } else {
240             return Response.status(Status.BAD_REQUEST).build();
241         }
242     }
243 
244     @POST
245     @Path("import")
246     @Consumes(MediaType.MULTIPART_FORM_DATA)
247     @Produces(MediaType.TEXT_HTML)
248     public Response importClassification(@FormDataParam("classificationFile") InputStream uploadedInputStream) {
249         try {
250             MCRClassificationUtils.fromStream(uploadedInputStream);
251         } catch (MCRAccessException accessExc) {
252             return Response.status(Status.UNAUTHORIZED).build();
253         } catch (Exception exc) {
254             throw new WebApplicationException(exc);
255         }
256         // This is a hack to support iframe loading via ajax.
257         // The benefit is to load file input form data without reloading the page.
258         // Maybe its better to create a separate method importClassificationIFrame.
259         // @see http://livedocs.dojotoolkit.org/dojo/io/iframe - Additional Information
260         return Response.ok("<html><body><textarea>200</textarea></body></html>").build();
261     }
262 
263     @GET
264     @Path("filter/{text}")
265     @Produces(MediaType.APPLICATION_JSON)
266     public Response filter(@PathParam("text") String text) {
267         SolrClient solrClient = MCRSolrClassificationUtil.getCore().getClient();
268         ModifiableSolrParams p = new ModifiableSolrParams();
269         p.set("q", "*" + text + "*");
270         p.set("fl", "id,ancestors");
271 
272         JsonArray docList = new JsonArray();
273         MCRSolrSearchUtils.stream(solrClient, p).flatMap(document -> {
274             List<String> ids = new ArrayList<>();
275             ids.add(document.getFirstValue("id").toString());
276             Collection<Object> fieldValues = document.getFieldValues("ancestors");
277             if (fieldValues != null) {
278                 for (Object anc : fieldValues) {
279                     ids.add(anc.toString());
280                 }
281             }
282             return ids.stream();
283         }).distinct().map(JsonPrimitive::new).forEach(docList::add);
284         return Response.ok().entity(docList.toString()).build();
285     }
286 
287     @GET
288     @Path("link/{id}")
289     @Produces(MediaType.APPLICATION_JSON)
290     public Response retrieveLinkedObjects(@PathParam("id") String id, @QueryParam("start") Integer start,
291         @QueryParam("rows") Integer rows) throws SolrServerException, IOException {
292         // do solr query
293         SolrClient solrClient = MCRSolrClientFactory.getMainSolrClient();
294         ModifiableSolrParams params = new ModifiableSolrParams();
295         params.set("start", start != null ? start : 0);
296         params.set("rows", rows != null ? rows : 50);
297         params.set("fl", "id");
298         String configQuery = MCRConfiguration2.getString("MCR.Solr.linkQuery").orElse("category.top:{0}");
299         String query = new MessageFormat(configQuery, Locale.ROOT).format(new String[] { id.replaceAll(":", "\\\\:") });
300         params.set("q", query);
301         QueryResponse solrResponse = solrClient.query(params);
302         SolrDocumentList solrResults = solrResponse.getResults();
303         // build json response
304         JsonObject response = new JsonObject();
305         response.addProperty("numFound", solrResults.getNumFound());
306         response.addProperty("start", solrResults.getStart());
307         JsonArray docList = new JsonArray();
308         for (SolrDocument doc : solrResults) {
309             docList.add(new JsonPrimitive((String) doc.getFieldValue("id")));
310         }
311         response.add("docs", docList);
312         return Response.ok().entity(response.toString()).build();
313     }
314 
315     protected MCRCategoryID newRootID() {
316         String uuid = UUID.randomUUID().toString().replaceAll("-", "");
317         return MCRCategoryID.rootID(uuid);
318     }
319 
320     private MCRCategoryID newRandomUUID(String rootID) {
321         String newRootID = rootID;
322         if (rootID == null) {
323             newRootID = UUID.randomUUID().toString();
324         }
325         return new MCRCategoryID(newRootID, UUID.randomUUID().toString());
326     }
327 
328     private String getCategory(MCRCategoryID id) {
329         if (!CATEGORY_DAO.exist(id)) {
330             throw new WebApplicationException(Status.NOT_FOUND);
331         }
332 
333         MCRCategory category = CATEGORY_DAO.getCategory(id, 1);
334         if (!(category instanceof MCRJSONCategory)) {
335             category = new MCRJSONCategory(category);
336         }
337         Gson gson = MCRJSONManager.instance().createGson();
338 
339         return gson.toJson(category);
340     }
341 
342     private SaveElement getCateg(JsonElement jsonElement) {
343         JsonObject jsonObject = jsonElement.getAsJsonObject();
344         JsonObject categ = jsonObject.get("item").getAsJsonObject();
345         JsonElement parentID = jsonObject.get("parentId");
346         JsonElement position = jsonObject.get("index");
347         boolean hasParent = false;
348 
349         if (parentID != null && !parentID.toString().contains("_placeboid_") && position != null) {
350             categ.add(MCRJSONCategoryHelper.PROP_PARENTID, parentID);
351             categ.add(MCRJSONCategoryHelper.PROP_POSITION, position);
352             hasParent = true;
353         }
354 
355         return new SaveElement(categ.toString(), hasParent);
356     }
357 
358     private String getStatus(JsonElement jsonElement) {
359         return jsonElement.getAsJsonObject().get("state").getAsString();
360     }
361 
362     private boolean isAdded(JsonElement jsonElement) {
363         JsonElement added = jsonElement.getAsJsonObject().get("added");
364         return added != null && jsonElement.getAsJsonObject().get("added").getAsBoolean();
365     }
366 
367     private MCRJSONCategory parseJson(String json) {
368         Gson gson = MCRJSONManager.instance().createGson();
369         return gson.fromJson(json, MCRJSONCategory.class);
370     }
371 
372     protected String buildJsonError(String errorType, MCRCategoryID mcrCategoryID) {
373         Gson gson = MCRJSONManager.instance().createGson();
374         JsonObject error = new JsonObject();
375         error.addProperty("type", errorType);
376         error.addProperty("rootid", mcrCategoryID.getRootID());
377         error.addProperty("categid", mcrCategoryID.getID());
378         return gson.toJson(error);
379     }
380 
381     private static class SaveElement {
382         private String categJson;
383 
384         private boolean hasParent;
385 
386         SaveElement(String categJson, boolean hasParent) {
387             this.setCategJson(categJson);
388             this.setHasParent(hasParent);
389         }
390 
391         private void setHasParent(boolean hasParent) {
392             this.hasParent = hasParent;
393         }
394 
395         @SuppressWarnings("unused")
396         public boolean hasParent() {
397             return hasParent;
398         }
399 
400         private void setCategJson(String categJson) {
401             this.categJson = categJson;
402         }
403 
404         public String getJson() {
405             return categJson;
406         }
407     }
408 
409     private static class IndexComperator implements Comparator<JsonElement> {
410         @Override
411         public int compare(JsonElement jsonElement1, JsonElement jsonElement2) {
412             if (!jsonElement1.isJsonObject()) {
413                 return 1;
414             }
415             if (!jsonElement2.isJsonObject()) {
416                 return -1;
417             }
418             // compare level first
419             JsonPrimitive depthLevel1 = jsonElement1.getAsJsonObject().getAsJsonPrimitive("depthLevel");
420             JsonPrimitive depthLevel2 = jsonElement2.getAsJsonObject().getAsJsonPrimitive("depthLevel");
421             if (depthLevel1 == null) {
422                 return 1;
423             }
424             if (depthLevel2 == null) {
425                 return -1;
426             }
427             if (depthLevel1.getAsInt() != depthLevel2.getAsInt()) {
428                 return Integer.compare(depthLevel1.getAsInt(), depthLevel2.getAsInt());
429             }
430             // compare index
431             JsonPrimitive index1 = jsonElement1.getAsJsonObject().getAsJsonPrimitive("index");
432             JsonPrimitive index2 = jsonElement2.getAsJsonObject().getAsJsonPrimitive("index");
433             if (index1 == null) {
434                 return 1;
435             }
436             if (index2 == null) {
437                 return -1;
438             }
439             return Integer.compare(index1.getAsInt(), index2.getAsInt());
440         }
441     }
442 
443     interface OperationInSession {
444         void run();
445     }
446 
447     private class DeleteOp implements OperationInSession {
448 
449         private MCRJSONCategory category;
450 
451         private Response response;
452 
453         DeleteOp(MCRJSONCategory category) {
454             this.category = category;
455         }
456 
457         @Override
458         public void run() {
459             MCRCategoryID categoryID = category.getId();
460             if (CATEGORY_DAO.exist(categoryID)) {
461                 if (categoryID.isRootID()
462                     && !MCRAccessManager.checkPermission(categoryID.getRootID(), PERMISSION_DELETE)) {
463                     throw new WebApplicationException(Status.UNAUTHORIZED);
464                 }
465                 CATEGORY_DAO.deleteCategory(categoryID);
466                 setResponse(Response.status(Status.GONE).build());
467             } else {
468                 setResponse(Response.notModified().build());
469             }
470         }
471 
472         public Response getResponse() {
473             return response;
474         }
475 
476         private void setResponse(Response response) {
477             this.response = response;
478         }
479 
480     }
481 
482     private class UpdateOp implements OperationInSession {
483 
484         private MCRJSONCategory category;
485 
486         private JsonObject jsonObject;
487 
488         UpdateOp(MCRJSONCategory category, JsonObject jsonObject) {
489             this.category = category;
490             this.jsonObject = jsonObject;
491         }
492 
493         @Override
494         public void run() {
495             MCRCategoryID mcrCategoryID = category.getId();
496             boolean isAdded = isAdded(jsonObject);
497             if (isAdded && MCRCategoryDAOFactory.getInstance().exist(mcrCategoryID)) {
498                 // an added category already exist -> throw conflict error
499                 throw new WebApplicationException(
500                     Response.status(Status.CONFLICT).entity(buildJsonError("duplicateID", mcrCategoryID)).build());
501             }
502 
503             MCRCategoryID newParentID = category.getParentID();
504             if (newParentID != null && !CATEGORY_DAO.exist(newParentID)) {
505                 throw new WebApplicationException(Status.NOT_FOUND);
506             }
507             if (CATEGORY_DAO.exist(category.getId())) {
508                 CATEGORY_DAO.setLabels(category.getId(), category.getLabels());
509                 CATEGORY_DAO.setURI(category.getId(), category.getURI());
510                 if (newParentID != null) {
511                     CATEGORY_DAO.moveCategory(category.getId(), newParentID, category.getPositionInParent());
512                 }
513             } else {
514                 CATEGORY_DAO.addCategory(newParentID, category.asMCRImpl(), category.getPositionInParent());
515             }
516         }
517 
518     }
519 }