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  package org.mycore.restapi.v1.utils;
19  
20  import static org.mycore.access.MCRAccessManager.PERMISSION_WRITE;
21  
22  import java.io.BufferedInputStream;
23  import java.io.BufferedWriter;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.StringWriter;
27  import java.nio.charset.StandardCharsets;
28  import java.nio.file.Files;
29  import java.nio.file.Paths;
30  import java.nio.file.StandardCopyOption;
31  import java.util.HashSet;
32  import java.util.List;
33  import java.util.Optional;
34  import java.util.Set;
35  import java.util.SortedMap;
36  import java.util.TreeMap;
37  import java.util.function.Predicate;
38  import java.util.stream.Collectors;
39  import java.util.stream.Stream;
40  import java.util.zip.ZipEntry;
41  import java.util.zip.ZipInputStream;
42  
43  import org.apache.logging.log4j.LogManager;
44  import org.apache.logging.log4j.Logger;
45  import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
46  import org.jdom2.Document;
47  import org.jdom2.input.SAXBuilder;
48  import org.jdom2.output.Format;
49  import org.jdom2.output.XMLOutputter;
50  import org.mycore.access.MCRAccessException;
51  import org.mycore.access.MCRAccessManager;
52  import org.mycore.common.MCRPersistenceException;
53  import org.mycore.common.config.MCRConfiguration2;
54  import org.mycore.datamodel.classifications2.MCRCategoryDAO;
55  import org.mycore.common.MCRUtils;
56  import org.mycore.datamodel.classifications2.MCRCategoryDAOFactory;
57  import org.mycore.datamodel.classifications2.MCRCategoryID;
58  import org.mycore.datamodel.metadata.MCRDerivate;
59  import org.mycore.datamodel.metadata.MCRMetaClassification;
60  import org.mycore.datamodel.metadata.MCRMetaEnrichedLinkID;
61  import org.mycore.datamodel.metadata.MCRMetaEnrichedLinkIDFactory;
62  import org.mycore.datamodel.metadata.MCRMetaIFS;
63  import org.mycore.datamodel.metadata.MCRMetaLangText;
64  import org.mycore.datamodel.metadata.MCRMetaLinkID;
65  import org.mycore.datamodel.metadata.MCRMetadataManager;
66  import org.mycore.datamodel.metadata.MCRObject;
67  import org.mycore.datamodel.metadata.MCRObjectID;
68  import org.mycore.datamodel.niofs.MCRPath;
69  import org.mycore.datamodel.niofs.utils.MCRRecursiveDeleter;
70  import org.mycore.datamodel.niofs.utils.MCRTreeCopier;
71  import org.mycore.frontend.cli.MCRObjectCommands;
72  import org.mycore.restapi.v1.errors.MCRRestAPIError;
73  import org.mycore.restapi.v1.errors.MCRRestAPIException;
74  
75  import jakarta.servlet.http.HttpServletRequest;
76  import jakarta.ws.rs.core.Response;
77  import jakarta.ws.rs.core.UriInfo;
78  import jakarta.ws.rs.core.Response.Status;
79  
80  public class MCRRestAPIUploadHelper {
81      private static final Logger LOGGER = LogManager.getLogger(MCRRestAPIUploadHelper.class);
82  
83      private static java.nio.file.Path UPLOAD_DIR = Paths
84          .get(MCRConfiguration2.getStringOrThrow("MCR.RestAPI.v1.Upload.Directory"));
85  
86      static {
87          if (!Files.exists(UPLOAD_DIR)) {
88              try {
89                  Files.createDirectories(UPLOAD_DIR);
90              } catch (IOException e) {
91                  LOGGER.error(e);
92              }
93          }
94      }
95  
96      /**
97       *
98       * uploads a MyCoRe Object
99       * based upon:
100      * http://puspendu.wordpress.com/2012/08/23/restful-webservice-file-upload-with-jersey/
101      * 
102      * @param info - the Jersey UriInfo object
103      * @param request - the HTTPServletRequest object 
104      * @param uploadedInputStream - the inputstream from HTTP Post request
105      * @param fileDetails - the file information from HTTP Post request
106      * @return a Jersey Response object
107      * @throws MCRRestAPIException
108      */
109     public static Response uploadObject(UriInfo info, HttpServletRequest request, InputStream uploadedInputStream,
110         FormDataContentDisposition fileDetails) throws MCRRestAPIException {
111 
112         java.nio.file.Path fXML = null;
113         try {
114             SAXBuilder sb = new SAXBuilder();
115             Document docOut = sb.build(uploadedInputStream);
116 
117             MCRObjectID mcrID = MCRObjectID.getInstance(docOut.getRootElement().getAttributeValue("ID"));
118             if (mcrID.getNumberAsInteger() == 0) {
119                 mcrID = MCRObjectID.getNextFreeId(mcrID.getBase());
120             }
121 
122             fXML = UPLOAD_DIR.resolve(mcrID + ".xml");
123 
124             docOut.getRootElement().setAttribute("ID", mcrID.toString());
125             docOut.getRootElement().setAttribute("label", mcrID.toString());
126             XMLOutputter xmlOut = new XMLOutputter(Format.getPrettyFormat());
127             try (BufferedWriter bw = Files.newBufferedWriter(fXML, StandardCharsets.UTF_8)) {
128                 xmlOut.output(docOut, bw);
129             }
130 
131             MCRObjectCommands.updateFromFile(fXML.toString(), false); // handles "create" as well
132 
133             return Response.created(info.getBaseUriBuilder().path("objects/" + mcrID).build())
134                 .type("application/xml; charset=UTF-8")
135                 .build();
136         } catch (Exception e) {
137             LOGGER.error("Unable to Upload file: {}", String.valueOf(fXML), e);
138             throw new MCRRestAPIException(Status.BAD_REQUEST, new MCRRestAPIError(MCRRestAPIError.CODE_WRONG_PARAMETER,
139                 "Unable to Upload file: " + fXML, e.getMessage()));
140         } finally {
141             if (fXML != null) {
142                 try {
143                     Files.delete(fXML);
144                 } catch (IOException e) {
145                     LOGGER.error("Unable to delete temporary workflow file: {}", String.valueOf(fXML), e);
146                 }
147             }
148         }
149     }
150 
151     /**
152      * creates or updates a MyCoRe derivate
153      * @param info - the Jersey UriInfo object
154      * @param request - the HTTPServletRequest object 
155      * @param mcrObjID - the MyCoRe Object ID
156      * @param label - the label of the new derivate
157      * @param overwriteOnExisting, if true, an existing MyCoRe derivate
158      *        with the given label or classification will be returned 
159      * @return a Jersey Response object
160      * @throws MCRRestAPIException
161      */
162     public static Response uploadDerivate(UriInfo info, HttpServletRequest request, String mcrObjID, String label,
163         String classifications, boolean overwriteOnExisting) throws MCRRestAPIException {
164         Response response = Response.status(Status.INTERNAL_SERVER_ERROR).build();
165 
166         //  File fXML = null;
167         MCRObjectID mcrObjIDObj = MCRObjectID.getInstance(mcrObjID);
168 
169         try {
170             MCRObject mcrObj = MCRMetadataManager.retrieveMCRObject(mcrObjIDObj);
171             MCRObjectID derID = null;
172             final MCRCategoryDAO dao = MCRCategoryDAOFactory.getInstance();
173             if (overwriteOnExisting) {
174                 final List<MCRMetaEnrichedLinkID> currentDerivates = mcrObj.getStructure().getDerivates();
175                 if (label != null && label.length() > 0) {
176                     for (MCRMetaLinkID derLink : currentDerivates) {
177                         if (label.equals(derLink.getXLinkLabel()) || label.equals(derLink.getXLinkTitle())) {
178                             derID = derLink.getXLinkHrefID();
179                         }
180                     }
181                 }
182                 if (derID == null && classifications != null && classifications.length() > 0) {
183                     final List<MCRCategoryID> categories = Stream.of(classifications.split(" "))
184                         .map(MCRCategoryID::fromString)
185                         .collect(Collectors.toList());
186 
187                     final List<MCRCategoryID> notExisting = categories.stream().filter(Predicate.not(dao::exist))
188                         .collect(Collectors.toList());
189 
190                     if (notExisting.size() > 0) {
191                         final String missingIDS = notExisting.stream()
192                             .map(MCRCategoryID::toString).collect(Collectors.joining(", "));
193                         throw new MCRRestAPIException(Status.NOT_FOUND,
194                             new MCRRestAPIError(MCRRestAPIError.CODE_NOT_FOUND, "Classification not found.",
195                                 "There are no classification with the IDs: " + missingIDS));
196                     }
197                     final Optional<MCRMetaEnrichedLinkID> matchingDerivate = currentDerivates.stream()
198                         .filter(derLink -> {
199                             final Set<MCRCategoryID> clazzSet = new HashSet<>(derLink.getClassifications());
200                             return categories.stream().allMatch(clazzSet::contains);
201                         }).findFirst();
202                     if (matchingDerivate.isPresent()) {
203                         derID = matchingDerivate.get().getXLinkHrefID();
204                     }
205                 }
206             }
207 
208             if (derID == null) {
209                 derID = MCRObjectID.getNextFreeId(mcrObjIDObj.getProjectId() + "_derivate");
210                 MCRDerivate mcrDerivate = new MCRDerivate();
211                 if (label != null && label.length() > 0) {
212                     mcrDerivate.getDerivate().getTitles()
213                         .add(new MCRMetaLangText("title", null, null, 0, null, label));
214                 }
215                 mcrDerivate.setId(derID);
216                 mcrDerivate.setSchema("datamodel-derivate.xsd");
217                 mcrDerivate.getDerivate().setLinkMeta(new MCRMetaLinkID("linkmeta", mcrObjIDObj, null, null));
218                 mcrDerivate.getDerivate().setInternals(new MCRMetaIFS("internal", null));
219 
220                 if (classifications != null && classifications.length() > 0) {
221                     final List<MCRMetaClassification> currentClassifications;
222                     currentClassifications = mcrDerivate.getDerivate().getClassifications();
223                     Stream.of(classifications.split(" "))
224                         .map(MCRCategoryID::fromString)
225                         .filter(dao::exist)
226                         .map(categoryID -> new MCRMetaClassification("classification", 0, null, categoryID))
227                         .forEach(currentClassifications::add);
228                 }
229 
230                 MCRMetadataManager.create(mcrDerivate);
231                 MCRMetadataManager.addOrUpdateDerivateToObject(mcrObjIDObj,
232                     MCRMetaEnrichedLinkIDFactory.getInstance().getDerivateLink(mcrDerivate));
233             }
234 
235             response = Response
236                 .created(info.getBaseUriBuilder().path("objects/" + mcrObjID + "/derivates/" + derID).build())
237                 .type("application/xml; charset=UTF-8")
238                 .build();
239         } catch (Exception e) {
240             LOGGER.error("Exeption while uploading derivate", e);
241         }
242         return response;
243     }
244 
245     /**
246      * uploads a file into a given derivate
247      * @param info - the Jersey UriInfo object
248      * @param request - the HTTPServletRequest object 
249      * @param pathParamMcrObjID - a MyCoRe Object ID
250      * @param pathParamMcrDerID - a MyCoRe Derivate ID
251      * @param uploadedInputStream - the inputstream from HTTP Post request
252      * @param fileDetails - the file information from HTTP Post request
253      * @param formParamPath - the path of the file inside the derivate
254      * @param formParamMaindoc - true, if this file should be marked as maindoc
255      * @param formParamUnzip - true, if the upload is zip file that should be unzipped inside the derivate
256      * @param formParamMD5 - the MD5 sum of the uploaded file 
257      * @param formParamSize - the size of the uploaded file
258      * @return a Jersey Response object
259      * @throws MCRRestAPIException
260      */
261     public static Response uploadFile(UriInfo info, HttpServletRequest request, String pathParamMcrObjID,
262         String pathParamMcrDerID, InputStream uploadedInputStream, FormDataContentDisposition fileDetails,
263         String formParamPath, boolean formParamMaindoc, boolean formParamUnzip, String formParamMD5,
264         Long formParamSize) throws MCRRestAPIException {
265 
266         SortedMap<String, String> parameter = new TreeMap<>();
267         parameter.put("mcrObjectID", pathParamMcrObjID);
268         parameter.put("mcrDerivateID", pathParamMcrDerID);
269         parameter.put("path", formParamPath);
270         parameter.put("maindoc", Boolean.toString(formParamMaindoc));
271         parameter.put("unzip", Boolean.toString(formParamUnzip));
272         parameter.put("md5", formParamMD5);
273         parameter.put("size", Long.toString(formParamSize));
274 
275         MCRObjectID objID = MCRObjectID.getInstance(pathParamMcrObjID);
276         MCRObjectID derID = MCRObjectID.getInstance(pathParamMcrDerID);
277 
278         if (!MCRAccessManager.checkPermission(derID.toString(), PERMISSION_WRITE)) {
279             throw new MCRRestAPIException(Status.FORBIDDEN,
280                 new MCRRestAPIError(MCRRestAPIError.CODE_ACCESS_DENIED, "Could not add file to derivate",
281                     "You do not have the permission to write to " + derID));
282         }
283         MCRDerivate der = MCRMetadataManager.retrieveMCRDerivate(derID);
284 
285         java.nio.file.Path derDir = null;
286 
287         String path = null;
288         if (!der.getOwnerID().equals(objID)) {
289             throw new MCRRestAPIException(Status.INTERNAL_SERVER_ERROR,
290                 new MCRRestAPIError(MCRRestAPIError.CODE_INTERNAL_ERROR, "Derivate object mismatch",
291                     "Derivate " + derID + " belongs to a different object: " + objID));
292         }
293         try {
294             derDir = UPLOAD_DIR.resolve(derID.toString());
295             if (Files.exists(derDir)) {
296                 Files.walkFileTree(derDir, MCRRecursiveDeleter.instance());
297             }
298             path = formParamPath.replace("\\", "/").replace("../", "");
299             while (path.startsWith("/")) {
300                 path = path.substring(1);
301             }
302 
303             MCRPath derRoot = MCRPath.getPath(derID.toString(), "/");
304             if (Files.notExists(derRoot)) {
305                 derRoot.getFileSystem().createRoot(derID.toString());
306             }
307             
308             if (formParamUnzip) {
309                 String maindoc = null;
310                 try (ZipInputStream zis = new ZipInputStream(new BufferedInputStream(uploadedInputStream))) {
311                     ZipEntry entry;
312                     while ((entry = zis.getNextEntry()) != null) {
313                         LOGGER.debug("Unzipping: {}", entry.getName());
314                         java.nio.file.Path target = MCRUtils.safeResolve(derDir, entry.getName());
315                         Files.createDirectories(target.getParent());
316                         Files.copy(zis, target, StandardCopyOption.REPLACE_EXISTING);
317                         if (maindoc == null && !entry.isDirectory()) {
318                             maindoc = entry.getName();
319                         }
320                     }
321                 } catch (IOException e) {
322                     LOGGER.error(e);
323                 }
324 
325                 Files.walkFileTree(derDir, new MCRTreeCopier(derDir, derRoot, true));
326                 if (formParamMaindoc) {
327                     der.getDerivate().getInternals().setMainDoc(maindoc);
328                 }
329             } else {
330                 java.nio.file.Path saveFile = MCRUtils.safeResolve(derDir, path);
331                 Files.createDirectories(saveFile.getParent());
332                 Files.copy(uploadedInputStream, saveFile, StandardCopyOption.REPLACE_EXISTING);
333 
334                 Files.walkFileTree(derDir, new MCRTreeCopier(derDir, derRoot, true));
335                 if (formParamMaindoc) {
336                     der.getDerivate().getInternals().setMainDoc(path);
337                 }
338             }
339 
340             MCRMetadataManager.update(der);
341             Files.walkFileTree(derDir, MCRRecursiveDeleter.instance());
342         } catch (IOException | MCRPersistenceException | MCRAccessException e) {
343             LOGGER.error(e);
344             throw new MCRRestAPIException(Status.INTERNAL_SERVER_ERROR,
345                 new MCRRestAPIError(MCRRestAPIError.CODE_INTERNAL_ERROR, "Internal error", e.getMessage()));
346         }
347         return Response
348             .created(info.getBaseUriBuilder().path("objects/" + objID + "/derivates/" + derID + "/contents").build())
349             .type("application/xml; charset=UTF-8").build();
350     }
351 
352     /**
353      * deletes all files inside a given derivate
354      * @param info - the Jersey UriInfo object
355      * @param request - the HTTPServletRequest object 
356      * @param pathParamMcrObjID - the MyCoRe Object ID
357      * @param pathParamMcrDerID - the MyCoRe Derivate ID
358      * @return a Jersey Response Object
359      * @throws MCRRestAPIException
360      */
361     public static Response deleteAllFiles(UriInfo info, HttpServletRequest request, String pathParamMcrObjID,
362         String pathParamMcrDerID) throws MCRRestAPIException {
363 
364         SortedMap<String, String> parameter = new TreeMap<>();
365         parameter.put("mcrObjectID", pathParamMcrObjID);
366         parameter.put("mcrDerivateID", pathParamMcrDerID);
367 
368         MCRObjectID objID = MCRObjectID.getInstance(pathParamMcrObjID);
369         MCRObjectID derID = MCRObjectID.getInstance(pathParamMcrDerID);
370 
371         //MCRAccessManager.checkPermission uses CACHE, which seems to be dirty from other calls
372         MCRAccessManager.invalidPermissionCache(derID.toString(), PERMISSION_WRITE);
373         if (MCRAccessManager.checkPermission(derID.toString(), PERMISSION_WRITE)) {
374             MCRDerivate der = MCRMetadataManager.retrieveMCRDerivate(derID);
375 
376             final MCRPath rootPath = MCRPath.getPath(der.getId().toString(), "/");
377             try {
378                 Files.walkFileTree(rootPath, MCRRecursiveDeleter.instance());
379                 Files.createDirectory(rootPath);
380             } catch (IOException e) {
381                 LOGGER.error(e);
382             }
383         }
384 
385         return Response
386             .created(info.getBaseUriBuilder()
387                 .path("objects/" + objID + "/derivates/" + derID + "/contents")
388                 .build())
389             .type("application/xml; charset=UTF-8")
390             .build();
391     }
392 
393     /**
394      * deletes a whole derivate
395      * @param info - the Jersey UriInfo object
396      * @param request - the HTTPServletRequest object 
397      * @param pathParamMcrObjID - the MyCoRe Object ID
398      * @param pathParamMcrDerID - the MyCoRe Derivate ID
399      * @return a Jersey Response Object
400      * @throws MCRRestAPIException
401      */
402     public static Response deleteDerivate(UriInfo info, HttpServletRequest request, String pathParamMcrObjID,
403         String pathParamMcrDerID) throws MCRRestAPIException {
404 
405         MCRObjectID objID = MCRObjectID.getInstance(pathParamMcrObjID);
406         MCRObjectID derID = MCRObjectID.getInstance(pathParamMcrDerID);
407 
408         try {
409             MCRMetadataManager.deleteMCRDerivate(derID);
410             return Response
411                 .created(info.getBaseUriBuilder().path("objects/" + objID + "/derivates").build())
412                 .type("application/xml; charset=UTF-8")
413                 .build();
414         } catch (MCRAccessException e) {
415             throw new MCRRestAPIException(Status.FORBIDDEN,
416                 new MCRRestAPIError(MCRRestAPIError.CODE_ACCESS_DENIED, "Could not delete derivate", e.getMessage()));
417         }
418     }
419 
420     /**
421      * serializes a map of Strings into a compact JSON structure
422      * @param data a sorted Map of Strings 
423      * @return a compact JSON
424      */
425     public static String generateMessagesFromProperties(SortedMap<String, String> data) {
426         StringWriter sw = new StringWriter();
427         sw.append("{");
428         for (String key : data.keySet()) {
429             sw.append("\"").append(key).append("\"").append(":").append("\"").append(data.get(key)).append("\"")
430                 .append(",");
431         }
432         String result = sw.toString();
433         if (result.length() > 1) {
434             result = result.substring(0, result.length() - 1);
435         }
436         result = result + "}";
437 
438         return result;
439     }
440 
441 }