1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
219
220
221
222
223
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
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
290
291
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
496
497
498
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
518
519
520
521
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
552
553
554
555
556
557
558
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 }