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.sword;
20  
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.io.OutputStream;
24  import java.io.UnsupportedEncodingException;
25  import java.net.URI;
26  import java.net.URISyntaxException;
27  import java.net.URLDecoder;
28  import java.net.URLEncoder;
29  import java.nio.ByteBuffer;
30  import java.nio.channels.SeekableByteChannel;
31  import java.nio.file.DirectoryStream;
32  import java.nio.file.FileAlreadyExistsException;
33  import java.nio.file.FileSystem;
34  import java.nio.file.FileSystems;
35  import java.nio.file.FileVisitResult;
36  import java.nio.file.Files;
37  import java.nio.file.Path;
38  import java.nio.file.SimpleFileVisitor;
39  import java.nio.file.StandardCopyOption;
40  import java.nio.file.StandardOpenOption;
41  import java.nio.file.attribute.BasicFileAttributes;
42  import java.security.DigestInputStream;
43  import java.security.MessageDigest;
44  import java.security.NoSuchAlgorithmException;
45  import java.text.MessageFormat;
46  import java.util.ArrayList;
47  import java.util.Collection;
48  import java.util.Date;
49  import java.util.HashMap;
50  import java.util.LinkedHashSet;
51  import java.util.List;
52  import java.util.Locale;
53  import java.util.Optional;
54  import java.util.Set;
55  import java.util.regex.Matcher;
56  import java.util.regex.Pattern;
57  import java.util.stream.Stream;
58  import java.util.zip.Deflater;
59  
60  import org.apache.abdera.Abdera;
61  import org.apache.abdera.factory.Factory;
62  import org.apache.abdera.i18n.iri.IRI;
63  import org.apache.abdera.model.Entry;
64  import org.apache.abdera.model.Feed;
65  import org.apache.abdera.model.Link;
66  import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
67  import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
68  import org.apache.logging.log4j.LogManager;
69  import org.apache.logging.log4j.Logger;
70  import org.mycore.access.MCRAccessException;
71  import org.mycore.access.MCRRuleAccessInterface;
72  import org.mycore.access.MCRAccessManager;
73  import org.mycore.common.MCRException;
74  import org.mycore.common.MCRPersistenceException;
75  import org.mycore.common.MCRSession;
76  import org.mycore.common.MCRSessionMgr;
77  import org.mycore.common.MCRTransactionHelper;
78  import org.mycore.common.MCRUtils;
79  import org.mycore.common.config.MCRConfiguration2;
80  import org.mycore.common.config.MCRConfigurationException;
81  import org.mycore.common.xml.MCRXMLFunctions;
82  import org.mycore.datamodel.metadata.MCRDerivate;
83  import org.mycore.datamodel.metadata.MCRMetaIFS;
84  import org.mycore.datamodel.metadata.MCRMetaLinkID;
85  import org.mycore.datamodel.metadata.MCRMetadataManager;
86  import org.mycore.datamodel.metadata.MCRObject;
87  import org.mycore.datamodel.metadata.MCRObjectID;
88  import org.mycore.datamodel.metadata.MCRObjectService;
89  import org.mycore.datamodel.niofs.MCRPath;
90  import org.mycore.frontend.MCRFrontendUtil;
91  import org.mycore.sword.application.MCRSwordCollectionProvider;
92  import org.mycore.sword.application.MCRSwordObjectIDSupplier;
93  import org.swordapp.server.DepositReceipt;
94  import org.swordapp.server.MediaResource;
95  import org.swordapp.server.SwordError;
96  import org.swordapp.server.SwordServerException;
97  import org.swordapp.server.UriRegistry;
98  
99  public class MCRSwordUtil {
100 
101     private static final int COPY_BUFFER_SIZE = 32 * 1024;
102 
103     private static Logger LOGGER = LogManager.getLogger(MCRSwordUtil.class);
104 
105     public static MCRDerivate createDerivate(String documentID)
106         throws MCRPersistenceException, IOException, MCRAccessException {
107         final String projectId = MCRObjectID.getInstance(documentID).getProjectId();
108         MCRObjectID oid = MCRObjectID.getNextFreeId(projectId, "derivate");
109         final String derivateID = oid.toString();
110 
111         MCRDerivate derivate = new MCRDerivate();
112         derivate.setId(oid);
113 
114         String schema = MCRConfiguration2.getString("MCR.Metadata.Config.derivate")
115             .orElse("datamodel-derivate.xml")
116             .replaceAll(".xml", ".xsd");
117         derivate.setSchema(schema);
118 
119         MCRMetaLinkID linkId = new MCRMetaLinkID();
120         linkId.setSubTag("linkmeta");
121         linkId.setReference(documentID, null, null);
122         derivate.getDerivate().setLinkMeta(linkId);
123 
124         MCRMetaIFS ifs = new MCRMetaIFS();
125         ifs.setSubTag("internal");
126         ifs.setSourcePath(null);
127 
128         derivate.getDerivate().setInternals(ifs);
129 
130         LOGGER.debug("Creating new derivate with ID {}", derivateID);
131         MCRMetadataManager.create(derivate);
132 
133         if (MCRConfiguration2.getBoolean("MCR.Access.AddDerivateDefaultRule").orElse(true)) {
134             MCRRuleAccessInterface aclImpl = MCRAccessManager.getAccessImpl();
135             Collection<String> configuredPermissions = aclImpl.getAccessPermissionsFromConfiguration();
136             for (String permission : configuredPermissions) {
137                 MCRAccessManager.addRule(derivateID, permission, MCRAccessManager.getTrueRule(),
138                     "default derivate rule");
139             }
140         }
141 
142         final MCRPath rootDir = MCRPath.getPath(derivateID, "/");
143         if (Files.notExists(rootDir)) {
144             rootDir.getFileSystem().createRoot(derivateID);
145         }
146 
147         return derivate;
148     }
149 
150     public static MediaResource getZippedDerivateMediaResource(String object) {
151         final Path tempFile;
152 
153         try {
154             tempFile = Files.createTempFile("swordv2_", ".temp.zip");
155         } catch (IOException e) {
156             throw new MCRException("Could not create temp file!", e);
157         }
158 
159         try (OutputStream tempFileStream = Files.newOutputStream(tempFile);
160             ZipArchiveOutputStream zipOutputStream = new ZipArchiveOutputStream(tempFileStream)) {
161             zipOutputStream.setLevel(Deflater.BEST_COMPRESSION);
162 
163             final MCRPath root = MCRPath.getPath(object, "/");
164             addDirectoryToZip(zipOutputStream, root);
165         } catch (IOException e) {
166             throw new MCRException(e);
167         }
168 
169         MediaResource resultRessource;
170         InputStream is;
171         try {
172             is = new MCRDeleteFileOnCloseFilterInputStream(Files.newInputStream(tempFile), tempFile);
173             resultRessource = new MediaResource(is, MCRSwordConstants.MIME_TYPE_APPLICATION_ZIP,
174                 UriRegistry.PACKAGE_SIMPLE_ZIP);
175         } catch (IOException e) {
176             throw new MCRException("could not read from temp file!", e);
177         }
178         return resultRessource;
179     }
180 
181     private static void addDirectoryToZip(ZipArchiveOutputStream zipOutputStream, Path directory) {
182         MCRSession currentSession = MCRSessionMgr.getCurrentSession();
183 
184         try (DirectoryStream<Path> paths = Files.newDirectoryStream(directory)) {
185             paths.forEach(p -> {
186                 final boolean isDir = Files.isDirectory(p);
187                 final ZipArchiveEntry zipArchiveEntry;
188                 try {
189                     final String fileName = getFilename(p);
190                     LOGGER.info("Addding {} to zip file!", fileName);
191                     if (isDir) {
192                         addDirectoryToZip(zipOutputStream, p);
193                     } else {
194                         zipArchiveEntry = new ZipArchiveEntry(fileName);
195                         zipArchiveEntry.setSize(Files.size(p));
196                         zipOutputStream.putArchiveEntry(zipArchiveEntry);
197                         if (MCRTransactionHelper.isTransactionActive()) {
198                             MCRTransactionHelper.commitTransaction();
199                         }
200                         Files.copy(p, zipOutputStream);
201                         MCRTransactionHelper.beginTransaction();
202                         zipOutputStream.closeArchiveEntry();
203                     }
204                 } catch (IOException e) {
205                     LOGGER.error("Could not add path {}", p);
206                 }
207             });
208         } catch (IOException e) {
209             throw new MCRException(e);
210         }
211     }
212 
213     public static String getFilename(Path path) {
214         return '/' + path.getRoot().relativize(path).toString();
215     }
216 
217     /**
218      * Stores stream to temp file and checks md5
219      *
220      * @param inputStream the stream which holds the File
221      * @param checkMd5    the md5 to compare with (or null if no md5 check is needed)
222      * @return the path to the temp file
223      * @throws IOException if md5 does mismatch or if stream could not be read
224      */
225     public static Path createTempFileFromStream(String fileName, InputStream inputStream, String checkMd5)
226         throws IOException {
227         MCRSession currentSession = MCRSessionMgr.getCurrentSession();
228         if (MCRTransactionHelper.isTransactionActive()) {
229             MCRTransactionHelper.commitTransaction();
230         }
231 
232         final Path zipTempFile = Files.createTempFile("swordv2_", fileName);
233         MessageDigest md5Digest = null;
234 
235         if (checkMd5 != null) {
236             try {
237                 md5Digest = MessageDigest.getInstance("MD5");
238                 inputStream = new DigestInputStream(inputStream, md5Digest);
239             } catch (NoSuchAlgorithmException e) {
240                 MCRTransactionHelper.beginTransaction();
241                 throw new MCRConfigurationException("No MD5 available!", e);
242             }
243         }
244 
245         Files.copy(inputStream, zipTempFile, StandardCopyOption.REPLACE_EXISTING);
246 
247         if (checkMd5 != null) {
248             final String md5String = MCRUtils.toHexString(md5Digest.digest());
249             if (!md5String.equals(checkMd5)) {
250                 MCRTransactionHelper.beginTransaction();
251                 throw new IOException("MD5 mismatch, expected " + checkMd5 + " got " + md5String);
252             }
253         }
254 
255         MCRTransactionHelper.beginTransaction();
256         return zipTempFile;
257     }
258 
259     public static void extractZipToPath(Path zipFilePath, MCRPath target)
260         throws SwordError, IOException, NoSuchAlgorithmException, URISyntaxException {
261         LOGGER.info("Extracting zip: {}", zipFilePath);
262         try (FileSystem zipfs = FileSystems.newFileSystem(new URI("jar:" + zipFilePath.toUri()), new HashMap<>())) {
263             final Path sourcePath = zipfs.getPath("/");
264             Files.walkFileTree(sourcePath,
265                 new SimpleFileVisitor<Path>() {
266                     @Override
267                     public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
268                         throws IOException {
269                         final Path relativeSP = sourcePath.relativize(dir);
270                         //WORKAROUND for bug
271                         Path targetdir = relativeSP.getNameCount() == 0 ? target : target.resolve(relativeSP);
272                         try {
273                             Files.copy(dir, targetdir);
274                         } catch (FileAlreadyExistsException e) {
275                             if (!Files.isDirectory(targetdir)) {
276                                 throw e;
277                             }
278                         }
279                         return FileVisitResult.CONTINUE;
280                     }
281 
282                     @Override
283                     public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
284                         throws IOException {
285                         MCRSession currentSession = MCRSessionMgr.getCurrentSession();
286 
287                         LOGGER.info("Extracting: {}", file);
288                         Path targetFilePath = target.resolve(sourcePath.relativize(file));
289                         // WORKAROUND: copy is bad with IFS because fsnodes is locked until copy is completed
290                         // so we end the transaction after we got a byte channel, then we write the data
291                         // and before completion we start the transaction to let niofs write the md5 to the table
292                         try (SeekableByteChannel destinationChannel = Files.newByteChannel(targetFilePath,
293                             StandardOpenOption.WRITE, StandardOpenOption.SYNC, StandardOpenOption.CREATE);
294                             SeekableByteChannel sourceChannel = Files.newByteChannel(file, StandardOpenOption.READ)) {
295                             if (MCRTransactionHelper.isTransactionActive()) {
296                                 MCRTransactionHelper.commitTransaction();
297                             }
298                             ByteBuffer buffer = ByteBuffer.allocateDirect(COPY_BUFFER_SIZE);
299                             while (sourceChannel.read(buffer) != -1 || buffer.position() > 0) {
300                                 buffer.flip();
301                                 destinationChannel.write(buffer);
302                                 buffer.compact();
303                             }
304                         } finally {
305                             if (!MCRTransactionHelper.isTransactionActive()) {
306                                 MCRTransactionHelper.beginTransaction();
307                             }
308                         }
309 
310                         return FileVisitResult.CONTINUE;
311                     }
312                 });
313         }
314 
315     }
316 
317     public static List<MCRValidationResult> validateZipFile(final MCRFileValidator validator, Path zipFile)
318         throws IOException, URISyntaxException {
319         try (FileSystem zipfs = FileSystems.newFileSystem(new URI("jar:" + zipFile.toUri()), new HashMap<>())) {
320             final Path sourcePath = zipfs.getPath("/");
321             ArrayList<MCRValidationResult> validationResults = new ArrayList<>();
322             Files.walkFileTree(sourcePath, new SimpleFileVisitor<Path>() {
323                 @Override
324                 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
325                     MCRValidationResult validationResult = validator.validate(file);
326                     if (!validationResult.isValid()) {
327                         validationResults.add(validationResult);
328                     }
329 
330                     return FileVisitResult.CONTINUE;
331                 }
332             });
333             return validationResults;
334         }
335     }
336 
337     public static String encodeURLPart(String uri) {
338         try {
339             return URLEncoder.encode(uri, BuildLinkUtil.DEFAULT_URL_ENCODING);
340         } catch (UnsupportedEncodingException e) {
341             throw new MCRException(BuildLinkUtil.DEFAULT_URL_ENCODING + " is not supported!", e);
342         }
343     }
344 
345     public static String decodeURLPart(String uri) {
346         try {
347             if (uri == null) {
348                 return null;
349             }
350             return URLDecoder.decode(uri, BuildLinkUtil.DEFAULT_URL_ENCODING);
351         } catch (UnsupportedEncodingException e) {
352             throw new MCRException(BuildLinkUtil.DEFAULT_URL_ENCODING + " is not supported!", e);
353         }
354     }
355 
356     public static DepositReceipt buildDepositReceipt(IRI iri) throws SwordError {
357         DepositReceipt depositReceipt = new DepositReceipt();
358         depositReceipt.setEditIRI(iri);
359         return depositReceipt;
360     }
361 
362     public static void addDatesToEntry(Entry entry, MCRObject mcrObject) {
363         MCRObjectService serviceElement = mcrObject.getService();
364         ArrayList<String> flags = serviceElement.getFlags(MCRObjectService.FLAG_TYPE_CREATEDBY);
365         flags.addAll(serviceElement.getFlags(MCRObjectService.FLAG_TYPE_MODIFIEDBY));
366         Set<String> clearedFlags = new LinkedHashSet<>(flags);
367         clearedFlags.forEach(entry::addAuthor);
368 
369         Date modifyDate = serviceElement.getDate(MCRObjectService.DATE_TYPE_MODIFYDATE);
370         Date createDate = serviceElement.getDate(MCRObjectService.DATE_TYPE_CREATEDATE);
371         entry.setEdited(modifyDate);
372         entry.setPublished(createDate);
373     }
374 
375     public static MCRObject getMcrObjectForDerivateID(String requestDerivateID) {
376         final MCRObjectID objectID = MCRObjectID.getInstance(MCRXMLFunctions.getMCRObjectID(requestDerivateID));
377         return (MCRObject) MCRMetadataManager.retrieve(objectID);
378     }
379 
380     public static class ParseLinkUtil {
381 
382         public static final Pattern COLLECTION_IRI_PATTERN = Pattern.compile("([a-zA-Z0-9]+)/([0-9]*)");
383 
384         public static final Pattern COLLECTION_MCRID_IRI_PATTERN = Pattern
385             .compile("([a-zA-Z0-9]+)/([a-zA-Z0-9]+_[a-zA-Z0-9]+_[0-9]+)");
386 
387         public static final Pattern COLLECTION_DERIVATEID_IRI_PATTERN = Pattern
388             .compile("([a-zA-Z0-9]+)/([a-zA-Z0-9]+_[a-zA-Z0-9]+_[0-9]+)(/.+)?");
389 
390         private static String getXFromXIRI(IRI editIRI, Integer x, String iri, Pattern iriPattern) {
391             return getXFromXIRI(editIRI, x, iri, iriPattern, true);
392         }
393 
394         private static String getXFromXIRI(IRI editIRI, Integer x, String iri, Pattern iriPattern, boolean required) {
395             String[] urlParts = editIRI.toString().split(iri);
396 
397             if (urlParts.length < 2) {
398                 final String message = "Invalid " + iri + " : " + editIRI;
399                 throw new IllegalArgumentException(message);
400             }
401 
402             String uriPathAsString = urlParts[1];
403             Matcher matcher = iriPattern.matcher(uriPathAsString);
404             if (matcher.matches()) {
405                 if (matcher.groupCount() >= x) {
406                     return matcher.group(x);
407                 } else {
408                     return null;
409                 }
410             } else {
411                 if (required) {
412                     throw new IllegalArgumentException(
413                         new MessageFormat("{0} does not match the pattern {1}", Locale.ROOT)
414                             .format(new Object[] { uriPathAsString, iriPattern }));
415                 } else {
416                     return null;
417                 }
418             }
419         }
420 
421         public static class CollectionIRI {
422 
423             public static String getCollectionNameFromCollectionIRI(IRI collectionIRI) {
424                 String uriPathAsString = collectionIRI.getPath().split(MCRSwordConstants.SWORD2_COL_IRI)[1];
425                 Matcher matcher = COLLECTION_IRI_PATTERN.matcher(uriPathAsString);
426                 if (matcher.matches()) {
427                     return matcher.group(1);
428                 } else {
429                     throw new IllegalArgumentException(
430                         new MessageFormat("{0} does not match the pattern {1}", Locale.ROOT)
431                             .format(new Object[] { uriPathAsString, COLLECTION_IRI_PATTERN }));
432                 }
433             }
434 
435             public static Integer getPaginationFromCollectionIRI(IRI collectionIRI) {
436                 String uriPathAsString = collectionIRI.getPath().split(MCRSwordConstants.SWORD2_COL_IRI)[1];
437                 Matcher matcher = COLLECTION_IRI_PATTERN.matcher(uriPathAsString);
438                 if (matcher.matches() && matcher.groupCount() > 1) {
439                     String numberGroup = matcher.group(2);
440                     if (numberGroup.length() > 0) {
441                         return Integer.parseInt(numberGroup);
442                     }
443                 }
444                 return 1;
445             }
446         }
447 
448         public static class EditIRI {
449 
450             public static String getCollectionFromEditIRI(IRI editIRI) {
451                 return getXFromXIRI(editIRI, 1, MCRSwordConstants.SWORD2_EDIT_IRI, COLLECTION_MCRID_IRI_PATTERN);
452             }
453 
454             public static String getObjectFromEditIRI(IRI editIRI) {
455                 return getXFromXIRI(editIRI, 2, MCRSwordConstants.SWORD2_EDIT_IRI, COLLECTION_MCRID_IRI_PATTERN);
456             }
457         }
458 
459         public static class MediaEditIRI {
460 
461             public static String getCollectionFromMediaEditIRI(IRI mediaEditIRI) {
462                 return getXFromXIRI(mediaEditIRI, 1, MCRSwordConstants.SWORD2_EDIT_MEDIA_IRI,
463                     COLLECTION_DERIVATEID_IRI_PATTERN);
464             }
465 
466             public static String getDerivateFromMediaEditIRI(IRI mediaEditIRI) {
467                 return getXFromXIRI(mediaEditIRI, 2, MCRSwordConstants.SWORD2_EDIT_MEDIA_IRI,
468                     COLLECTION_DERIVATEID_IRI_PATTERN);
469             }
470 
471             public static String getFilePathFromMediaEditIRI(IRI mediaEditIRI) {
472                 return decodeURLPart(getXFromXIRI(mediaEditIRI, 3, MCRSwordConstants.SWORD2_EDIT_MEDIA_IRI,
473                     COLLECTION_DERIVATEID_IRI_PATTERN, false));
474             }
475         }
476     }
477 
478     public static class BuildLinkUtil {
479 
480         public static final String DEFAULT_URL_ENCODING = "UTF-8";
481 
482         private static Logger LOGGER = LogManager.getLogger(BuildLinkUtil.class);
483 
484         public static String getEditHref(String collection, String id) {
485             return new MessageFormat("{0}{1}{2}/{3}", Locale.ROOT).format(
486                 new Object[] { MCRFrontendUtil.getBaseURL(), MCRSwordConstants.SWORD2_EDIT_IRI, collection, id });
487         }
488 
489         public static String getEditMediaHrefOfDerivate(String collection, String id) {
490             return new MessageFormat("{0}{1}{2}/{3}", Locale.ROOT).format(
491                 new Object[] { MCRFrontendUtil.getBaseURL(), MCRSwordConstants.SWORD2_EDIT_MEDIA_IRI, collection, id });
492         }
493 
494         /**
495          * Creates a edit link for every derivate of a mcrobject.
496          *
497          * @param mcrObjId the mcrobject id as String
498          * @return returns a Stream which contains links to every derivate.
499          */
500         public static Stream<Link> getEditMediaIRIStream(final String collection, final String mcrObjId)
501             throws SwordError {
502             return MCRSword.getCollection(collection).getDerivateIDsofObject(mcrObjId).map(derivateId -> {
503                 final Factory abderaFactory = Abdera.getNewFactory();
504                 final Stream<IRI> editMediaFileIRIStream = getEditMediaFileIRIStream(collection, derivateId);
505                 return Stream
506                     .concat(Stream.of(getEditMediaHrefOfDerivate(collection, derivateId)), editMediaFileIRIStream)
507                     .map(link -> {
508                         final Link newLinkElement = abderaFactory.newLink();
509                         newLinkElement.setHref(link.toString());
510                         newLinkElement.setRel("edit-media");
511                         return newLinkElement;
512                     });
513             }).flatMap(s -> s);
514         }
515 
516         /**
517          * Creates a Stream which contains edit-media-IRIs to all files in a specific derivate derivate.
518          *
519          * @param collection the collection in which the derivate is.
520          * @param derivateId the id of the derivate
521          * @return a Stream which contains edit-media-IRIs to all files.
522          */
523         private static Stream<IRI> getEditMediaFileIRIStream(final String collection, final String derivateId) {
524             MCRPath derivateRootPath = MCRPath.getPath(derivateId, "/");
525             try {
526                 List<IRI> iris = new ArrayList<>();
527                 Files.walkFileTree(derivateRootPath, new SimpleFileVisitor<Path>() {
528                     @Override
529                     public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
530                         throws IOException {
531                         String relativePath = derivateRootPath.relativize(file).toString();
532                         final String uri = new MessageFormat("{0}{1}{2}/{3}/{4}", Locale.ROOT).format(
533                             new Object[] { MCRFrontendUtil.getBaseURL(), MCRSwordConstants.SWORD2_EDIT_MEDIA_IRI,
534                                 collection, derivateId, encodeURLPart(relativePath) });
535                         iris.add(new IRI(uri));
536                         return FileVisitResult.CONTINUE;
537                     }
538                 });
539                 return iris.stream();
540             } catch (IOException e) {
541                 LOGGER.error("Error while processing directory stream of {}", derivateId, e);
542                 throw new MCRException(e);
543             }
544         }
545 
546         public static String buildCollectionPaginationLinkHref(String collection, Integer page) {
547             return MCRFrontendUtil.getBaseURL() + MCRSwordConstants.SWORD2_COL_IRI + collection + "/" + page;
548         }
549 
550         /**
551          * Creates Pagination links
552          *
553          * @param collectionIRI      IRI of the collection
554          * @param collection         name of the collection
555          * @param feed               the feed where the link will be inserted
556          * @param collectionProvider 
557          * {@link MCRSwordCollectionProvider} of the collection (needed to count how much objects)
558          * @throws SwordServerException when the {@link MCRSwordObjectIDSupplier} throws a exception.
559          */
560         public static void addPaginationLinks(IRI collectionIRI, String collection, Feed feed,
561             MCRSwordCollectionProvider collectionProvider) throws SwordServerException {
562             final int lastPage = (int) Math.ceil((double) collectionProvider.getIDSupplier().getCount()
563                 / (double) MCRSwordConstants.MAX_ENTRYS_PER_PAGE);
564             Integer currentPage = ParseLinkUtil.CollectionIRI.getPaginationFromCollectionIRI(collectionIRI);
565 
566             feed.addLink(buildCollectionPaginationLinkHref(collection, 1), "first");
567             if (lastPage != currentPage) {
568                 feed.addLink(buildCollectionPaginationLinkHref(collection, currentPage + 1), "next");
569             }
570             feed.addLink(buildCollectionPaginationLinkHref(collection, lastPage), "last");
571         }
572 
573         static void addEditMediaLinks(String collection, DepositReceipt depositReceipt, MCRObjectID derivateId) {
574             getEditMediaFileIRIStream(collection, derivateId.toString()).forEach(depositReceipt::addEditMediaIRI);
575         }
576     }
577 
578     public interface MCRFileValidator {
579         MCRValidationResult validate(Path pathToFile);
580     }
581 
582     public static class MCRValidationResult {
583         private boolean valid;
584 
585         private Optional<String> message;
586 
587         public MCRValidationResult(boolean valid, String message) {
588             this.valid = valid;
589             this.message = Optional.ofNullable(message);
590         }
591 
592         public boolean isValid() {
593             return valid;
594         }
595 
596         public void setValid(boolean valid) {
597             this.valid = valid;
598         }
599 
600         public Optional<String> getMessage() {
601             return message;
602         }
603 
604         public void setMessage(String message) {
605             this.message = Optional.ofNullable(message);
606         }
607     }
608 
609 }