From 4ee6a88af9c8813c8d3cc99c49f56c40412f12ff Mon Sep 17 00:00:00 2001 From: Matthias Eichner Date: Wed, 31 Jan 2024 17:58:55 +0100 Subject: [PATCH 01/10] MCR-3035 fix empty p tags in wcms --- .../java/org/mycore/tools/MyCoReWebPageProvider.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/mycore-base/src/main/java/org/mycore/tools/MyCoReWebPageProvider.java b/mycore-base/src/main/java/org/mycore/tools/MyCoReWebPageProvider.java index 941efac48f..a18fdd6ede 100644 --- a/mycore-base/src/main/java/org/mycore/tools/MyCoReWebPageProvider.java +++ b/mycore-base/src/main/java/org/mycore/tools/MyCoReWebPageProvider.java @@ -89,7 +89,7 @@ public class MyCoReWebPageProvider { public static final String TIME_FORMAT = "HH:mm"; - private Document xml; + private final Document xml; public MyCoReWebPageProvider() { this.xml = new Document(); @@ -149,11 +149,14 @@ public Element addSection(String title, List contentList, String lang) if (lang != null) { section.setAttribute(XML_LANG, lang, Namespace.XML_NAMESPACE); } - if (title != null && !title.equals("")) { + if (title != null && !title.isEmpty()) { section.setAttribute(XML_TITLE, title); } for(Content content : contentList) { - if(content instanceof Text) { + if(content instanceof Text text) { + if(text.getTextTrim().isEmpty()) { + continue; + } // MyCoReWebPage.xsl ignores single text content -> wrap it in a p section.addContent(new Element("p").addContent(content)); } else { From 6e801942803f088b967ff201f248a9007a55e45a Mon Sep 17 00:00:00 2001 From: Sebastian Hofmann Date: Tue, 6 Feb 2024 13:28:51 +0100 Subject: [PATCH 02/10] MCR-3038 MCRMETSDefaultGenerator.structureMets generates double logical ids --- .../mycore/mets/model/MCRMETSDefaultGenerator.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/mycore-mets/src/main/java/org/mycore/mets/model/MCRMETSDefaultGenerator.java b/mycore-mets/src/main/java/org/mycore/mets/model/MCRMETSDefaultGenerator.java index df01e8dc66..c045f9c617 100644 --- a/mycore-mets/src/main/java/org/mycore/mets/model/MCRMETSDefaultGenerator.java +++ b/mycore-mets/src/main/java/org/mycore/mets/model/MCRMETSDefaultGenerator.java @@ -31,6 +31,7 @@ import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; +import java.util.concurrent.atomic.AtomicInteger; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -132,7 +133,8 @@ private Mets createMets() throws IOException { StructLink structLink = new StructLink(); // create internal structure - structureMets(getDerivatePath(), getIgnorePaths(), fileSec, physicalDiv, logicalDiv, structLink, 0); + structureMets(getDerivatePath(), getIgnorePaths(), fileSec, physicalDiv, logicalDiv, structLink, + new AtomicInteger(0)); hrefIdMap.clear(); // add to mets @@ -147,8 +149,7 @@ private Mets createMets() throws IOException { } private void structureMets(MCRPath dir, Set ignoreNodes, FileSec fileSec, PhysicalDiv physicalDiv, - LogicalDiv logicalDiv, StructLink structLink, int logOrder) throws IOException { - int lOrder = logOrder; + LogicalDiv logicalDiv, StructLink structLink, AtomicInteger logCounter) throws IOException { SortedMap files = new TreeMap<>(); SortedMap directories = new TreeMap<>(); @@ -160,11 +161,12 @@ private void structureMets(MCRPath dir, Set ignoreNodes, FileSec fileSe for (Map.Entry directory : directories.entrySet()) { String dirName = directory.getKey().getFileName().toString(); if (isInExcludedRootFolder(directory.getKey())) { - structureMets(directory.getKey(), ignoreNodes, fileSec, physicalDiv, logicalDiv, structLink, lOrder); + structureMets(directory.getKey(), ignoreNodes, fileSec, physicalDiv, logicalDiv, structLink, + logCounter); } else { - LogicalDiv section = new LogicalDiv("log_" + ++lOrder, "section", dirName); + LogicalDiv section = new LogicalDiv("log_" + logCounter.incrementAndGet(), "section", dirName); logicalDiv.add(section); - structureMets(directory.getKey(), ignoreNodes, fileSec, physicalDiv, section, structLink, lOrder); + structureMets(directory.getKey(), ignoreNodes, fileSec, physicalDiv, section, structLink, logCounter); } } } From a70c778347c445486fb0b9384673c27ee819030e Mon Sep 17 00:00:00 2001 From: Possommi Date: Wed, 7 Feb 2024 11:52:10 +0100 Subject: [PATCH 03/10] MCR-3037 Added getUsableSpace() to MCRXMLFunctions (#2058) * MCR-3037 Added getUsableSpace() to MCRXMLFunctions * MCR-3037 Use getStringOrThrow() rather that get() on MCRConfiguration2 --- .../org/mycore/common/xml/MCRXMLFunctions.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/mycore-base/src/main/java/org/mycore/common/xml/MCRXMLFunctions.java b/mycore-base/src/main/java/org/mycore/common/xml/MCRXMLFunctions.java index 715cf74cec..93942a8080 100644 --- a/mycore-base/src/main/java/org/mycore/common/xml/MCRXMLFunctions.java +++ b/mycore-base/src/main/java/org/mycore/common/xml/MCRXMLFunctions.java @@ -25,9 +25,11 @@ import java.net.URISyntaxException; import java.net.URL; import java.net.URLEncoder; +import java.nio.file.FileStore; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.text.Normalizer; @@ -842,7 +844,7 @@ public static boolean isHtml(final String s) { } /** - * Strippes HTML tags from string. + * Stripes HTML tags from string. * * @param s string to strip HTML tags of * @return the plain text without tags @@ -856,6 +858,20 @@ public static String stripHtml(final String s) { return StringEscapeUtils.unescapeHtml4(res.toString()).replaceAll(TAG_SELF_CLOSING, ""); } + /** + * Returns approximately the usable space in the MCR.datadir in bytes as in {@link FileStore#getUsableSpace()}. + * + * @return approximately the usable space in the MCR.datadir + * @throws IOException + * */ + public static long getUsableSpace() throws IOException{ + Path dataDir = Paths.get(MCRConfiguration2.getStringOrThrow("MCR.datadir")); + dataDir = dataDir.toRealPath(); + FileStore fs = Files.getFileStore(dataDir); + + return fs.getUsableSpace(); + } + /** * Converts a string to valid NCName. * From 263ce0fecb54b1cf91408f12d4e45592dc58ac51 Mon Sep 17 00:00:00 2001 From: Sebastian Hofmann <7668803+sebhofmann@users.noreply.github.com> Date: Tue, 13 Feb 2024 14:58:53 +0100 Subject: [PATCH 04/10] MCR-3039 fix upload size detection and fix error messages (#2059) * MCR-3039 fix upload size detection and fix error messages * MCR-3039 extract new translation keys to constant * MCR-3039 fix file upload validation * use ready check and status instead of onerror to check for errors * move error messages to the file upload window instead of alert() * use FileSystemFileEntry.file() to detect file size (should now work in firefox) * make error message corners rounded * MCR-3039 Improved types and added log for special case fileEntry == null * MCR-3039 fix drag and drop events which contains strings which lead to npe * MCR-3039 allow multiselect(already intended by code) and optimized validation message display --- .../upload/MCRDefaultUploadHandler.java | 60 +++++++------ .../exception/MCRInvalidFileException.java | 16 +++- .../MCRInvalidUploadParameterException.java | 10 ++- .../MCRUploadForbiddenException.java | 8 +- .../webtools/config/messages_de.properties | 10 ++- .../webtools/config/messages_en.properties | 8 +- .../main/ts/upload/src/FileTransferQueue.ts | 9 ++ .../src/main/ts/upload/src/TransferSession.ts | 22 ++--- .../src/main/ts/upload/src/UploadTarget.ts | 88 +++++++++++++------ .../src/main/ts/upload/src/upload-gui.ts | 8 +- 10 files changed, 165 insertions(+), 74 deletions(-) diff --git a/mycore-webtools/src/main/java/org/mycore/webtools/upload/MCRDefaultUploadHandler.java b/mycore-webtools/src/main/java/org/mycore/webtools/upload/MCRDefaultUploadHandler.java index f75da6e081..f4269a8576 100644 --- a/mycore-webtools/src/main/java/org/mycore/webtools/upload/MCRDefaultUploadHandler.java +++ b/mycore-webtools/src/main/java/org/mycore/webtools/upload/MCRDefaultUploadHandler.java @@ -33,6 +33,7 @@ import org.mycore.access.MCRAccessManager; import org.mycore.common.MCRException; import org.mycore.common.MCRPersistenceException; +import org.mycore.common.MCRUtils; import org.mycore.common.config.MCRConfiguration2; import org.mycore.datamodel.metadata.MCRDerivate; import org.mycore.datamodel.metadata.MCRMetaClassification; @@ -72,6 +73,12 @@ public class MCRDefaultUploadHandler implements MCRUploadHandler { public static final String CLASSIFICATIONS_PARAMETER_NAME = "classifications"; private static final Logger LOGGER = LogManager.getLogger(); + public static final String UNIQUE_OBJECT_TRANSLATION_KEY = "component.webtools.upload.invalid.parameter.unique"; + public static final String INVALID_OBJECT_TRANSLATION_KEY = "component.webtools.upload.invalid.parameter.object"; + public static final String OBJECT_DOES_NOT_EXIST_TRANSLATION_KEY + = "component.webtools.upload.invalid.parameter.object.not.exist"; + public static final String INVALID_FILE_NAME_TRANSLATION_KEY = "component.webtools.upload.invalid.fileName"; + public static final String INVALID_FILE_SIZE_TRANSLATION_KEY = "component.webtools.upload.invalid.fileSize"; public static void setDefaultMainFile(MCRDerivate derivate) { MCRPath path = MCRPath.getPath(derivate.getId().toString(), "/"); @@ -139,17 +146,40 @@ public URI commit(MCRFileUploadBucket bucket) throws MCRUploadServerException { return null; // We donĀ“t want to redirect to the derivate, so we return null } + private static void checkPermissions(MCRObjectID oid) throws MCRInvalidUploadParameterException, + MCRUploadForbiddenException { + if (!MCRMetadataManager.exists(oid)) { + throw new MCRInvalidUploadParameterException(OBJ_OR_DERIVATE_ID_PARAMETER_NAME, oid.toString(), + OBJECT_DOES_NOT_EXIST_TRANSLATION_KEY, true); + } + + if (!oid.getTypeId().equals("derivate")) { + try { + String formattedNewDerivateIDString = MCRObjectID.formatID(oid.getProjectId(), "derivate", 0); + MCRObjectID newDerivateId = MCRObjectID.getInstance(formattedNewDerivateIDString); + MCRMetadataManager.checkCreatePrivilege(newDerivateId); + } catch (MCRAccessException e) { + throw new MCRUploadForbiddenException(); + } + } + + if (!MCRAccessManager.checkPermission(oid, MCRAccessManager.PERMISSION_WRITE)) { + throw new MCRUploadForbiddenException(); + } + } + @Override public void validateFileMetadata(String name, long size) throws MCRInvalidFileException { try { MCRUploadHelper.checkPathName(name); } catch (MCRException e) { - throw new MCRInvalidFileException(name, e.getMessage()); + throw new MCRInvalidFileException(name, INVALID_FILE_NAME_TRANSLATION_KEY, true); } long maxSize = MCRConfiguration2.getOrThrow("MCR.FileUpload.MaxSize", Long::parseLong); if (size > maxSize) { - throw new MCRInvalidFileException(name, "Maximum allowed size is " + maxSize + ", size is " + size); + throw new MCRInvalidFileException(name, INVALID_FILE_SIZE_TRANSLATION_KEY, true, + MCRUtils.getSizeFormatted(size), MCRUtils.getSizeFormatted(maxSize)); } } @@ -163,13 +193,13 @@ public String begin(Map> parameters) List oidList = parameters.get(OBJ_OR_DERIVATE_ID_PARAMETER_NAME); if (oidList.size() != 1) { throw new MCRInvalidUploadParameterException(OBJ_OR_DERIVATE_ID_PARAMETER_NAME, String.join(",", oidList), - "There must be exactly one object or derivate id"); + UNIQUE_OBJECT_TRANSLATION_KEY, true); } String oidString = oidList.get(0); if (!MCRObjectID.isValid(oidString)) { throw new MCRInvalidUploadParameterException(OBJ_OR_DERIVATE_ID_PARAMETER_NAME, oidString, - "Invalid object or derivate id given"); + INVALID_OBJECT_TRANSLATION_KEY, true); } MCRObjectID oid = MCRObjectID.getInstance(oidString); @@ -179,28 +209,6 @@ public String begin(Map> parameters) return UUID.randomUUID().toString(); } - private static void checkPermissions(MCRObjectID oid) throws MCRInvalidUploadParameterException, - MCRUploadForbiddenException { - if (!MCRMetadataManager.exists(oid)) { - throw new MCRInvalidUploadParameterException(OBJ_OR_DERIVATE_ID_PARAMETER_NAME, oid.toString(), - "does not exist!"); - } - - if (!oid.getTypeId().equals("derivate")) { - try { - String formattedNewDerivateIDString = MCRObjectID.formatID(oid.getProjectId(), "derivate", 0); - MCRObjectID newDerivateId = MCRObjectID.getInstance(formattedNewDerivateIDString); - MCRMetadataManager.checkCreatePrivilege(newDerivateId); - } catch (MCRAccessException e) { - throw new MCRUploadForbiddenException("No rights to create derivate for " + oid + "!"); - } - } - - if (!MCRAccessManager.checkPermission(oid, MCRAccessManager.PERMISSION_WRITE)) { - throw new MCRUploadForbiddenException("No write access to " + oid); - } - } - /** * returns the object id from the parameters under the key {@link #OBJ_OR_DERIVATE_ID_PARAMETER_NAME} * @param parameters the parameters diff --git a/mycore-webtools/src/main/java/org/mycore/webtools/upload/exception/MCRInvalidFileException.java b/mycore-webtools/src/main/java/org/mycore/webtools/upload/exception/MCRInvalidFileException.java index d0eab7d0d3..0d96358dc3 100644 --- a/mycore-webtools/src/main/java/org/mycore/webtools/upload/exception/MCRInvalidFileException.java +++ b/mycore-webtools/src/main/java/org/mycore/webtools/upload/exception/MCRInvalidFileException.java @@ -18,6 +18,8 @@ package org.mycore.webtools.upload.exception; +import org.mycore.services.i18n.MCRTranslation; + /** * Should be thrown if the file is not valid. E.g. the size is too big or the file name contains invalid characters. */ @@ -29,8 +31,20 @@ public class MCRInvalidFileException extends MCRUploadException { private final String reason; + public MCRInvalidFileException(String fileName) { + super("component.webtools.upload.invalid.file.noReason", fileName); + this.fileName = fileName; + this.reason = null; + } + public MCRInvalidFileException(String fileName, String reason) { - super("component.webtools.upload.invalid.file", fileName, reason); + this(fileName, reason, false); + } + + public MCRInvalidFileException(String fileName, String reason, boolean translateReason, + Object... translationParams) { + super("component.webtools.upload.invalid.file", fileName, + translateReason ? MCRTranslation.translate(reason, translationParams) : reason); this.fileName = fileName; this.reason = reason; } diff --git a/mycore-webtools/src/main/java/org/mycore/webtools/upload/exception/MCRInvalidUploadParameterException.java b/mycore-webtools/src/main/java/org/mycore/webtools/upload/exception/MCRInvalidUploadParameterException.java index 7f13cd5c73..a25e4723fe 100644 --- a/mycore-webtools/src/main/java/org/mycore/webtools/upload/exception/MCRInvalidUploadParameterException.java +++ b/mycore-webtools/src/main/java/org/mycore/webtools/upload/exception/MCRInvalidUploadParameterException.java @@ -18,6 +18,8 @@ package org.mycore.webtools.upload.exception; +import org.mycore.services.i18n.MCRTranslation; + /** * Should be thrown if a parameter required by an upload handler is not valid. E.g. a classification does not exist. */ @@ -32,7 +34,13 @@ public class MCRInvalidUploadParameterException extends MCRUploadException { private final String badValue; public MCRInvalidUploadParameterException(String parameterName, String badValue, String wrongReason) { - super("component.webtools.upload.invalid.parameter", parameterName, badValue, wrongReason); + this(parameterName, badValue, wrongReason, false); + } + + public MCRInvalidUploadParameterException(String parameterName, String badValue, String wrongReason, + boolean translateReason) { + super("component.webtools.upload.invalid.parameter", parameterName, badValue, + translateReason ? MCRTranslation.translate(wrongReason) : wrongReason); this.parameterName = parameterName; this.wrongReason = wrongReason; this.badValue = badValue; diff --git a/mycore-webtools/src/main/java/org/mycore/webtools/upload/exception/MCRUploadForbiddenException.java b/mycore-webtools/src/main/java/org/mycore/webtools/upload/exception/MCRUploadForbiddenException.java index 3de69a9bf6..e465738d41 100644 --- a/mycore-webtools/src/main/java/org/mycore/webtools/upload/exception/MCRUploadForbiddenException.java +++ b/mycore-webtools/src/main/java/org/mycore/webtools/upload/exception/MCRUploadForbiddenException.java @@ -22,14 +22,14 @@ public class MCRUploadForbiddenException extends MCRUploadException { private static final long serialVersionUID = 1L; - private final String reason; + public MCRUploadForbiddenException(String reason) { super("component.webtools.upload.forbidden", reason); - this.reason = reason; } - public String getReason() { - return reason; + public MCRUploadForbiddenException() { + super("component.webtools.upload.forbidden.noReason"); } + } diff --git a/mycore-webtools/src/main/resources/components/webtools/config/messages_de.properties b/mycore-webtools/src/main/resources/components/webtools/config/messages_de.properties index 054b337aa8..333cd20e7f 100644 --- a/mycore-webtools/src/main/resources/components/webtools/config/messages_de.properties +++ b/mycore-webtools/src/main/resources/components/webtools/config/messages_de.properties @@ -16,13 +16,19 @@ component.webtools.upload.title = Datei-Upload component.webtools.upload.processing = Das Hochladen der Dateien wird auf dem Server abgeschlossen. Schlie\u00DFen Sie dieses Fenster nicht! component.webtools.upload.validating = Die Datei-Namen und -Gr\u00F6\u00DFen werden \u00FCberpr\u00FCft, bevor die Dateien hochgeladen werden. Bitte warten... component.webtools.upload.error = Beim Hochladen ist ein Fehler aufgetreten: -component.webtools.upload.invalid.fileSize = Die Datei {0} ist zu gro\u00DF: {1} / {2} +component.webtools.upload.invalid.fileSize = Die Datei ist zu gro\u00DF: {0} / {1} +component.webtools.upload.invalid.fileName = Der Datei-Name enth\u00E4lt ung\u00FCltige Zeichen. component.webtools.upload.invalid.file = Die Datei {0} ist nicht valide: {1} +component.webtools.upload.invalid.file.noReason = Die Datei {0} ist nicht valide. component.webtools.upload.invalid.parameter = Der Parameter {0} mit dem wert {1} ist nicht valide: {2} component.webtools.upload.invalid.parameter.missing = Der Parameter {0} fehlt. +component.webtools.upload.invalid.parameter.unique = Es darf nur ein Parameter "object" existieren. +component.webtools.upload.invalid.parameter.object = Der Parameter "object" ist keine g\u00FCltige MyCoRe-Objekt-ID. +component.webtools.upload.invalid.parameter.object.not.exist = Das angegebene MyCoRe-Objekt existiert nicht. component.webtools.upload.forbidden = Das Hochladen ist nicht erlaubt: {0} +component.webtools.upload.forbidden.noReason = Das Hochladen ist nicht erlaubt. component.webtools.upload.temp.delete.failed = Das L\u00F6schen des tempor\u00E4ren Verzeichnisses ist fehlgeschlagen. -component.webtools.upload.temp.create.failed = Das Erstellen eines tempor\u00E4ren Ordner ist gescheitert! +component.webtools.upload.temp.create.failed = Das Erstellen des tempor\u00E4ren Verzeichnisses ist gescheitert! component.webtools.msie.learnMore=Mehr erfahren diff --git a/mycore-webtools/src/main/resources/components/webtools/config/messages_en.properties b/mycore-webtools/src/main/resources/components/webtools/config/messages_en.properties index 86018f2629..52542c80c9 100644 --- a/mycore-webtools/src/main/resources/components/webtools/config/messages_en.properties +++ b/mycore-webtools/src/main/resources/components/webtools/config/messages_en.properties @@ -16,11 +16,17 @@ component.webtools.upload.title = File Upload component.webtools.upload.processing = The file upload is processed on the server. Please do not close the browser window. component.webtools.upload.validating = The file names and sizes are validated on the server. Please wait... component.webtools.upload.error = Something went wrong with the Upload: -component.webtools.upload.invalid.fileSize = The File {0} is to Big: {1} / {2} +component.webtools.upload.invalid.fileSize = The File is to Big: {0} / {1} +component.webtools.upload.invalid.fileName = The name of the file contains invalid characters. component.webtools.upload.invalid.file = The File {0} is not valid: {1} +component.webtools.upload.invalid.file.noReason = The File {0} is not valid. component.webtools.upload.invalid.parameter = The Parameter {0} is not valid: {1} component.webtools.upload.invalid.parameter.missing = The Parameter {0} is missing +component.webtools.upload.invalid.parameter.unique = There must be only one parameter "object". +component.webtools.upload.invalid.parameter.object = The Parameter "object" is not a valid MyCoRe-Object-ID. +component.webtools.upload.invalid.parameter.object.not.exist = The given MyCoRe-Object does not exist. component.webtools.upload.forbidden = The Upload is not allowed: {0} +component.webtools.upload.forbidden.noReason = The Upload is not allowed. component.webtools.upload.temp.delete.failed = The temporary folder could not be deleted! component.webtools.upload.temp.create.failed = The temporary folder could not be created! diff --git a/mycore-webtools/src/main/ts/upload/src/FileTransferQueue.ts b/mycore-webtools/src/main/ts/upload/src/FileTransferQueue.ts index 3513032770..94bb20b419 100644 --- a/mycore-webtools/src/main/ts/upload/src/FileTransferQueue.ts +++ b/mycore-webtools/src/main/ts/upload/src/FileTransferQueue.ts @@ -103,6 +103,7 @@ export class FileTransferQueue { private uploadIDCount: {} = {}; private _validating: boolean; private validatingHandlerList:Array<(validating: boolean) => void > = []; + private validationHandlerList: Array<(message: string) => void> = []; constructor() { } @@ -192,6 +193,14 @@ export class FileTransferQueue { return this._validating; } + public pushValidationError(message: string) { + this.validationHandlerList.forEach(handler => handler(message)); + } + + public addValidationHandler(handler: (message: string) => void) { + this.validationHandlerList.push(handler); + } + public addCompleteHandler(handler: FileTransferHandler) { this.completeHandlerList.push(handler); } diff --git a/mycore-webtools/src/main/ts/upload/src/TransferSession.ts b/mycore-webtools/src/main/ts/upload/src/TransferSession.ts index 0527c2bb93..e262ba7165 100644 --- a/mycore-webtools/src/main/ts/upload/src/TransferSession.ts +++ b/mycore-webtools/src/main/ts/upload/src/TransferSession.ts @@ -66,21 +66,21 @@ export class TransferSession { this.request.open('PUT', Utils.getUploadSettings().webAppBaseURL + "rsc/files/upload/begin" + uploadHandlerParameter + parameters, true); this.request.onreadystatechange = (result) => { - if (this.request.readyState === 4 && this.request.status === 200) { - this._bucketID = this.request.responseText; - this._started = true; - if (completionHandler) { - completionHandler(); + if (this.request.readyState === 4) { + if(this.request.status === 200) { + this._bucketID = this.request.responseText; + this._started = true; + if (completionHandler) { + completionHandler(); + } + } else { + if (errorHandler) { + errorHandler(this.request.responseText); + } } } }; - this.request.onerror = (evt) => { - if (errorHandler) { - errorHandler(this.request.responseText); - } - }; - this.request.send(); } diff --git a/mycore-webtools/src/main/ts/upload/src/UploadTarget.ts b/mycore-webtools/src/main/ts/upload/src/UploadTarget.ts index 7e07d86a7d..8e73c221cc 100644 --- a/mycore-webtools/src/main/ts/upload/src/UploadTarget.ts +++ b/mycore-webtools/src/main/ts/upload/src/UploadTarget.ts @@ -87,25 +87,29 @@ export class UploadTarget { } - + fileInput.multiple = true; fileInput.setAttribute("type", "file"); fileInput.addEventListener('change', async () => { - let transferSession = FileTransferQueue.getQueue().registerTransferSession(this.uploadHandler, + const queue = FileTransferQueue.getQueue(); + let transferSession = queue.registerTransferSession(this.uploadHandler, this.attributes); + queue.setValidating(true); + const validFiles: File[] = []; for (let i = 0; i < fileInput.files.length; i++) { let file = fileInput.files.item(i); - FileTransferQueue.getQueue().setValidating(true); const validation = await this.validateTraverse(file); - FileTransferQueue.getQueue().setValidating(false); if (!validation.test) { - // TODO: show nice in GUI - alert(validation.reason); + queue.setValidating(false); + queue.pushValidationError(validation.reason); return true; } - const fileTransfer = new FileTransfer(file, this.target, transferSession, []); - FileTransferQueue.getQueue() - FileTransferQueue.getQueue().add(fileTransfer); + validFiles.push(file); } + queue.setValidating(false); + validFiles.forEach((file) => { + const fileTransfer = new FileTransfer(file, this.target, transferSession, []); + queue.add(fileTransfer); + }); }); if (!testing) { fileInput.click(); @@ -123,20 +127,38 @@ export class UploadTarget { const webkitEntries: any[] = []; for (let i = 0; i < items.length; i++) { - webkitEntries.push(items[i].webkitGetAsEntry()); + const item = items[i]; + if (item.kind !== "file") { + console.warn("Item is not a file", item); + continue; + } + let result: FileSystemEntry | null = null; + if ("webkitGetAsEntry" in item) { + result = item.webkitGetAsEntry(); + } else if ("getAsEntry" in item) { + // webkitGetAsEntry will be renamed to getAsEntry in the future + result = (item as any).getAsEntry(); + } + if (result != null) { + webkitEntries.push(result); + } + } + if (webkitEntries.length == 0) { + return false; } + FileTransferQueue.getQueue().setValidating(true); for (let i = 0; i < webkitEntries.length; i++) { const file = webkitEntries[i]; - FileTransferQueue.getQueue().setValidating(true); const validation = await this.validateTraverse(file); - FileTransferQueue.getQueue().setValidating(false); if (!validation.test) { - // TODO: show nice in GUI - alert(validation.reason); - return true; + FileTransferQueue.getQueue().setValidating(false); + FileTransferQueue.getQueue().pushValidationError(validation.reason); + return false; } } + FileTransferQueue.getQueue().setValidating(false); + for (let i = 0; i < webkitEntries.length; i++) { const file = webkitEntries[i]; this.traverse(ts, file); @@ -146,9 +168,17 @@ export class UploadTarget { return true; } - private async validateTraverse(fileEntry: any): Promise<{ test: boolean, fileEntry: any, reason: string | null }> { - if (fileEntry.isDirectory) { - let children = await this.readEntries(fileEntry); + private async validateTraverse(fileEntry: File | FileSystemEntry): Promise<{ + test: boolean, + fileEntry: any, + reason: string | null + }> { + if(fileEntry == null) { + console.error("File entry is null", fileEntry); + return {test: false, fileEntry: fileEntry, reason: "File entry is null"}; + } + if ("isDirectory" in fileEntry && fileEntry.isDirectory) { + let children = await this.readEntries(fileEntry as FileSystemDirectoryEntry); const promises: Array> = []; for (let childIndex in children) { const child = children[childIndex]; @@ -160,12 +190,12 @@ export class UploadTarget { const failed = results.find((result) => !result.test); return failed || {test: true, fileEntry, reason: null}; } else { - const validation = await this.validateFile(fileEntry); + const validation = await this.validateFile(fileEntry as FileSystemFileEntry | File); return {test: validation.valid, fileEntry, reason: validation.reason}; } } - private async readEntries(fileEntry: any): Promise { + private async readEntries(fileEntry: FileSystemDirectoryEntry): Promise { const reader = fileEntry.createReader(); return new Promise((accept, reject) => { @@ -187,7 +217,7 @@ export class UploadTarget { * @param fileEntry the entry which contains the name and size * @private */ - private async validateFile(fileEntry: File|FileSystemEntry): Promise<{ valid: boolean, reason: string | null }> { + private async validateFile(fileEntry: File | FileSystemEntry): Promise<{ valid: boolean, reason: string | null }> { const size = await this.getEntrySize(fileEntry); return new Promise((accept, reject) => { const isFileSystemEntry = 'fullPath' in fileEntry; @@ -223,13 +253,19 @@ export class UploadTarget { * @param fileEntry the file entry * @private */ - private async getEntrySize(fileEntry: any): Promise { + private async getEntrySize(fileEntry: File | FileSystemEntry): Promise { return new Promise((accept, reject) => { - if("getMetadata" in fileEntry){ - fileEntry.getMetadata((metadata) => { - accept(metadata.size); - }, (err) => reject(err)); + if("file" in fileEntry) { + (fileEntry as FileSystemFileEntry ).file((file) => { + accept(file.size); + }, (error) => { + console.error(["Error while reading file size", fileEntry, error]); + accept(-1); + }); + } else if("size" in fileEntry) { + accept(fileEntry.size); } else { + console.error(["Error while reading file size", fileEntry]); accept(-1); } }); diff --git a/mycore-webtools/src/main/ts/upload/src/upload-gui.ts b/mycore-webtools/src/main/ts/upload/src/upload-gui.ts index 33386d51d1..38cad7e129 100644 --- a/mycore-webtools/src/main/ts/upload/src/upload-gui.ts +++ b/mycore-webtools/src/main/ts/upload/src/upload-gui.ts @@ -110,6 +110,10 @@ export class FileTransferGUI { this.handleSessionBeginError(session, message); }); + queue.addValidationHandler((ft) => { + this.showError(ft); + }); + window.setInterval(() => { this.transferProgressMap.forEach((progress) => { this.setTransferProgress(this.getEntry(progress.transfer), progress.loaded, progress.total); @@ -273,7 +277,7 @@ export class FileTransferGUI { } } - private showError(message: string) { + public showError(message: string) { const error = this._uploadBox.querySelector(".mcr-commit-error"); if (error.classList.contains("d-none")) { error.classList.remove("d-none"); @@ -334,7 +338,7 @@ class FileTransferGUITemplates {
-
+
From 126caf49a97002259ea3d4520bc7ebc479bc143c Mon Sep 17 00:00:00 2001 From: Sebastian Hofmann Date: Thu, 8 Feb 2024 12:52:28 +0100 Subject: [PATCH 05/10] MCR-3040 fix XSLT 3 issues * Missing XSL3 version of IViewConfig-js.xsl * mcr-module-startIview2.xsl is missing in components-3 include * wrong include in response.xsl () --- .../iview2/config/mycore.properties | 1 + .../resources/xslt/solr/response/response.xsl | 2 +- .../main/resources/xslt/IViewConfig-js.xsl | 116 ++++++++++++++++++ 3 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 mycore-viewer/src/main/resources/xslt/IViewConfig-js.xsl diff --git a/mycore-iview2/src/main/resources/components/iview2/config/mycore.properties b/mycore-iview2/src/main/resources/components/iview2/config/mycore.properties index f3ceb61f56..2ffe6d9f1d 100644 --- a/mycore-iview2/src/main/resources/components/iview2/config/mycore.properties +++ b/mycore-iview2/src/main/resources/components/iview2/config/mycore.properties @@ -20,6 +20,7 @@ MCR.Module-iview2.MaxResetCount=3 MCR.CLI.Classes.Internal=%MCR.CLI.Classes.Internal%,org.mycore.iview2.frontend.MCRIView2Commands MCR.URIResolver.ModuleResolver.iview2=org.mycore.iview2.services.MCRIview2URIResolver MCR.URIResolver.xslIncludes.components=%MCR.URIResolver.xslIncludes.components%,mcr-module-startIview2.xsl +MCR.URIResolver.xslIncludes.components-3=%MCR.URIResolver.xslIncludes.components-3%,mcr-module-startIview2.xsl MCR.URIResolver.xslIncludes.solrResponse=%MCR.URIResolver.xslIncludes.solrResponse%,iview2-solrresponse.xsl MCR.URIResolver.xslIncludes.functions=%MCR.URIResolver.xslIncludes.functions%,functions/iview2.xsl diff --git a/mycore-solr/src/main/resources/xslt/solr/response/response.xsl b/mycore-solr/src/main/resources/xslt/solr/response/response.xsl index ce03b24bda..0efe94892b 100644 --- a/mycore-solr/src/main/resources/xslt/solr/response/response.xsl +++ b/mycore-solr/src/main/resources/xslt/solr/response/response.xsl @@ -6,7 +6,7 @@ xmlns:mcri18n="http://www.mycore.de/xslt/i18n" exclude-result-prefixes="mcri18n"> &html-output; - + diff --git a/mycore-viewer/src/main/resources/xslt/IViewConfig-js.xsl b/mycore-viewer/src/main/resources/xslt/IViewConfig-js.xsl new file mode 100644 index 0000000000..63492690ab --- /dev/null +++ b/mycore-viewer/src/main/resources/xslt/IViewConfig-js.xsl @@ -0,0 +1,116 @@ + + + + + + + + (function () { + window["viewerLoader"] = window["viewerLoader"] || (function () { + let executeOnReady = []; + let notLoadedCount = 0; + let scriptToLoad = []; + + let loader = { + loadedStyles: function () { + let existingCss = []; + for (let i = 0; i < document.styleSheets.length; i++) { + let css = document.styleSheets[i]; + let src = css.href; + if (src != null) { + existingCss.push(src); + } + } + return existingCss; + }, + addConstructorExecution: function (fn) { + executeOnReady.push(fn); + }, + getLoadedScripts: function () { + let existingScripts = []; + + for (let i = 0; i < document.scripts.length; i++) { + let script = document.scripts[i]; + let href = script.src; + if (href != null) { + existingScripts.push(href); + } + } + return existingScripts; + }, + addRequiredScripts: function (scripts, mobile) { + if(!mobile && !loader.isBootstrapPresent()){ + notLoadedCount++; + interval = window.setInterval(function(){ + if(loader.isBootstrapPresent()){ + notLoadedCount--; + window.clearInterval(interval); + loader.excecuteOnReadyFn(); + } + }, 50); + } + scripts.filter(function (script) { + return loader.getLoadedScripts().indexOf(script) === -1; + }).forEach(function (scriptSrc) { + let script = document.createElement('script'); + script.async = false; + notLoadedCount++; + + script.onload = function() { + notLoadedCount--; + loader.excecuteOnReadyFn(); + }; + script.async = false; + script.src = scriptSrc; + document.head.appendChild(script); + }); + + + }, + excecuteOnReadyFn: function(){ + if(notLoadedCount==0){ + let current; + while((current=executeOnReady.pop()) != null){ + current(); + } + } + }, + isBootstrapPresent: function(){ + return typeof $ !='undefined' && + typeof $.fn !='undefined' && + typeof $.fn.tooltip !='undefined' && + typeof $.fn.tooltip.Constructor!='undefined' && + typeof $.fn.tooltip.Constructor.VERSION!='undefined'; + }, + addRequiredCss: function (styles) { + styles.filter(function (s) { + return loader.loadedStyles().indexOf(s) === -1; + }).forEach(function (style) { + let link = document.createElement('link'); + link.rel = 'stylesheet'; + link.type = 'text/css'; + link.href = style; + link.media = 'all'; + document.head.appendChild(link); + }); + } + }; + + return loader; + })(); + +// viewer dependency loader + let configuration =; + + viewerLoader.addRequiredCss(configuration.resources.css); + viewerLoader.addRequiredScripts(configuration.resources.script, configuration.properties.mobile); + viewerLoader.addConstructorExecution(function(){ + let container = jQuery("[data-viewer='"+configuration.properties.derivate+":"+CSS.escape(configuration.properties.filePath)+"']"); + new mycore.viewer.MyCoReViewer(container, configuration.properties); + }); + +}) +(); + + + From eea87b4123931865e0fb6fa6a9986a2fa35c188d Mon Sep 17 00:00:00 2001 From: vs-gsi <107410707+vs-gsi@users.noreply.github.com> Date: Tue, 13 Feb 2024 15:13:18 +0100 Subject: [PATCH 06/10] MCR-3030 Isolated the HTTP-client in use by MCRRESTResolver (#2051) * isolated the http-client in use by MCRRESTResolver and allow to config-override the used class, consult the added MCRHTTPClient interface to see the simple abstraction * applied eclipse autoformat with active mycore preferences * cleaned everything for checkstyle, removed unused variable relict * restructured the MCRHTTPClient classes * http client config name in static var * MCRHTTPClient properties populated in config via annotations * MCR.URIResolver -> MCR.HTTPClient : added 3 properties as deprecated * MCRDefaultHTTPClient refactored get method for less "dangerous" return * MCRDefaultHTTPClient minor issue and license added * just removed comments * license in MCRHTTPClient interface --------- Co-authored-by: vsc --- .../mycore/common/MCRDefaultHTTPClient.java | 119 ++++++++++++++++++ .../java/org/mycore/common/MCRHTTPClient.java | 30 +++++ .../org/mycore/common/xml/MCRURIResolver.java | 76 ++--------- .../resources/config/deprecated.properties | 4 + .../main/resources/config/mycore.properties | 6 + 5 files changed, 167 insertions(+), 68 deletions(-) create mode 100644 mycore-base/src/main/java/org/mycore/common/MCRDefaultHTTPClient.java create mode 100644 mycore-base/src/main/java/org/mycore/common/MCRHTTPClient.java diff --git a/mycore-base/src/main/java/org/mycore/common/MCRDefaultHTTPClient.java b/mycore-base/src/main/java/org/mycore/common/MCRDefaultHTTPClient.java new file mode 100644 index 0000000000..1973e0f114 --- /dev/null +++ b/mycore-base/src/main/java/org/mycore/common/MCRDefaultHTTPClient.java @@ -0,0 +1,119 @@ +/* + * This file is part of *** M y C o R e *** + * See http://www.mycore.de/ for details. + * + * MyCoRe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * MyCoRe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MyCoRe. If not, see . + */ + +package org.mycore.common; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; + +import org.apache.http.client.cache.HttpCacheContext; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.cache.CacheConfig; +import org.apache.http.impl.client.cache.CachingHttpClients; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.mycore.common.config.annotation.MCRProperty; +import org.mycore.common.content.MCRContent; +import org.mycore.common.content.MCRStreamContent; +import org.mycore.common.events.MCRShutdownHandler; +import org.mycore.services.http.MCRHttpUtils; + +public class MCRDefaultHTTPClient implements MCRHTTPClient { + private static Logger logger = LogManager.getLogger(); + + private long maxObjectSize; + + private int maxCacheEntries; + + private int requestTimeout; + + private CloseableHttpClient restClient; + + public MCRDefaultHTTPClient() { + CacheConfig cacheConfig = CacheConfig.custom() + .setMaxObjectSize(maxObjectSize) + .setMaxCacheEntries(maxCacheEntries) + .build(); + RequestConfig requestConfig = RequestConfig.custom() + .setConnectTimeout(requestTimeout) + .setSocketTimeout(requestTimeout) + .build(); + this.restClient = CachingHttpClients.custom() + .setCacheConfig(cacheConfig) + .setDefaultRequestConfig(requestConfig) + .setUserAgent(MCRHttpUtils.getHttpUserAgent()) + .useSystemProperties() + .build(); + MCRShutdownHandler.getInstance().addCloseable(this::close); + } + + @MCRProperty(name = "MaxObjectSize") + public void setMaxObjectSize(String size) { + this.maxObjectSize = Long.parseLong(size); + } + + @MCRProperty(name = "MaxCacheEntries") + public void setMaxCacheEntries(String size) { + this.maxCacheEntries = Integer.parseInt(size); + } + + @MCRProperty(name = "RequestTimeout") + public void setRequestTimeout(String size) { + this.requestTimeout = Integer.parseInt(size); + } + + public void close() { + try { + restClient.close(); + } catch (IOException e) { + logger.warn("Exception while closing http client.", e); + } + } + + @Override + public MCRContent get(URI hrefURI) throws IOException { + HttpCacheContext context = HttpCacheContext.create(); + HttpGet get = new HttpGet(hrefURI); + MCRContent retContent = null; + try (CloseableHttpResponse response = restClient.execute(get, context); + InputStream content = response.getEntity().getContent();) { + logger.debug("http query: {}", hrefURI); + logger.debug("http resp status: {}", response.getStatusLine()); + logger.debug(() -> getCacheDebugMsg(hrefURI, context)); + retContent = (new MCRStreamContent(content)).getReusableCopy(); + } finally { + get.reset(); + } + return retContent; + } + + private String getCacheDebugMsg(URI hrefURI, HttpCacheContext context) { + return hrefURI.toASCIIString() + ": " + + switch (context.getCacheResponseStatus()) { + case CACHE_HIT -> "A response was generated from the cache with no requests sent upstream"; + case CACHE_MODULE_RESPONSE -> "The response was generated directly by the caching module"; + case CACHE_MISS -> "The response came from an upstream server"; + case VALIDATED -> "The response was generated from the cache after validating the entry " + + "with the origin server"; + }; + } +} diff --git a/mycore-base/src/main/java/org/mycore/common/MCRHTTPClient.java b/mycore-base/src/main/java/org/mycore/common/MCRHTTPClient.java new file mode 100644 index 0000000000..2b20326d58 --- /dev/null +++ b/mycore-base/src/main/java/org/mycore/common/MCRHTTPClient.java @@ -0,0 +1,30 @@ +/* + * This file is part of *** M y C o R e *** + * See http://www.mycore.de/ for details. + * + * MyCoRe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * MyCoRe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MyCoRe. If not, see . + */ + +package org.mycore.common; + +import java.io.IOException; +import java.net.URI; + +import org.mycore.common.content.MCRContent; + +public interface MCRHTTPClient { + MCRContent get(URI hrefURI) throws IOException; + + void close(); +} diff --git a/mycore-base/src/main/java/org/mycore/common/xml/MCRURIResolver.java b/mycore-base/src/main/java/org/mycore/common/xml/MCRURIResolver.java index bba946b247..4e074d8d7e 100644 --- a/mycore-base/src/main/java/org/mycore/common/xml/MCRURIResolver.java +++ b/mycore-base/src/main/java/org/mycore/common/xml/MCRURIResolver.java @@ -58,14 +58,7 @@ import javax.xml.xpath.XPathExpressionException; import org.apache.commons.lang3.StringUtils; -import org.apache.http.client.cache.HttpCacheContext; -import org.apache.http.client.config.RequestConfig; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpGet; import org.apache.http.client.utils.URLEncodedUtils; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.cache.CacheConfig; -import org.apache.http.impl.client.cache.CachingHttpClients; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Marker; @@ -82,6 +75,7 @@ import org.mycore.common.MCRCoreVersion; import org.mycore.common.MCRDeveloperTools; import org.mycore.common.MCRException; +import org.mycore.common.MCRHTTPClient; import org.mycore.common.MCRSessionMgr; import org.mycore.common.MCRUsageException; import org.mycore.common.MCRUserInformation; @@ -95,7 +89,6 @@ import org.mycore.common.content.transformer.MCRContentTransformer; import org.mycore.common.content.transformer.MCRParameterizedTransformer; import org.mycore.common.content.transformer.MCRXSLTransformer; -import org.mycore.common.events.MCRShutdownHandler; import org.mycore.common.xsl.MCRLazyStreamSource; import org.mycore.common.xsl.MCRParameterCollector; import org.mycore.datamodel.classifications2.MCRCategory; @@ -114,7 +107,6 @@ import org.mycore.datamodel.niofs.MCRPath; import org.mycore.datamodel.niofs.MCRPathXML; import org.mycore.frontend.MCRLayoutUtilities; -import org.mycore.services.http.MCRHttpUtils; import org.mycore.services.i18n.MCRTranslation; import org.mycore.tools.MCRObjectFactory; import org.xml.sax.InputSource; @@ -140,6 +132,8 @@ public final class MCRURIResolver implements URIResolver { private static final String CONFIG_PREFIX = "MCR.URIResolver."; + private static final String HTTP_CLIENT_CLASS = "MCR.HTTPClient.Class"; + private static final Marker UNIQUE_MARKER = MarkerManager.getMarker("tryResolveXML"); private static Map SUPPORTED_SCHEMES; @@ -539,77 +533,23 @@ public Source resolve(String href, String base) throws TransformerException { } private static class MCRRESTResolver implements URIResolver { - private static final long MAX_OBJECT_SIZE = MCRConfiguration2.getLong(CONFIG_PREFIX + "REST.MaxObjectSize") - .orElse(128 * 1024L); - - private static final int MAX_CACHE_ENTRIES = MCRConfiguration2.getInt(CONFIG_PREFIX + "REST.MaxCacheEntries") - .orElse(1000); - - private static final int REQUEST_TIMEOUT = MCRConfiguration2.getInt(CONFIG_PREFIX + "REST.RequestTimeout") - .orElse(30000); - - private CloseableHttpClient restClient; - - private org.apache.logging.log4j.Logger logger; + private MCRHTTPClient client; MCRRESTResolver() { - CacheConfig cacheConfig = CacheConfig.custom() - .setMaxObjectSize(MAX_OBJECT_SIZE) - .setMaxCacheEntries(MAX_CACHE_ENTRIES) - .build(); - RequestConfig requestConfig = RequestConfig.custom() - .setConnectTimeout(REQUEST_TIMEOUT) - .setSocketTimeout(REQUEST_TIMEOUT) - .build(); - this.restClient = CachingHttpClients.custom() - .setCacheConfig(cacheConfig) - .setDefaultRequestConfig(requestConfig) - .setUserAgent(MCRHttpUtils.getHttpUserAgent()) - .useSystemProperties() - .build(); - MCRShutdownHandler.getInstance().addCloseable(this::close); - this.logger = LogManager.getLogger(); - } - - public void close() { - try { - restClient.close(); - } catch (IOException e) { - LogManager.getLogger().warn("Exception while closing http client.", e); - } + this.client = (MCRHTTPClient) MCRConfiguration2.getInstanceOf(HTTP_CLIENT_CLASS).get(); } @Override public Source resolve(String href, String base) throws TransformerException { URI hrefURI = MCRURIResolver.resolveURI(href, base); try { - HttpCacheContext context = HttpCacheContext.create(); - HttpGet get = new HttpGet(hrefURI); - try (CloseableHttpResponse response = restClient.execute(get, context); - InputStream content = response.getEntity().getContent()) { - logger.debug(() -> getCacheDebugMsg(hrefURI, context)); - final Source source = new MCRStreamContent(content).getReusableCopy().getSource(); - source.setSystemId(hrefURI.toASCIIString()); - return source; - } finally { - get.reset(); - } + final Source source = client.get(hrefURI).getSource(); + source.setSystemId(hrefURI.toASCIIString()); + return source; } catch (IOException e) { throw new TransformerException(e); } } - - private String getCacheDebugMsg(URI hrefURI, HttpCacheContext context) { - return hrefURI.toASCIIString() + ": " + - switch (context.getCacheResponseStatus()) { - case CACHE_HIT -> "A response was generated from the cache with no requests sent upstream"; - case CACHE_MODULE_RESPONSE -> "The response was generated directly by the caching module"; - case CACHE_MISS -> "The response came from an upstream server"; - case VALIDATED -> "The response was generated from the cache after validating the entry " - + "with the origin server"; - }; - } - } private static class MCRObjectResolver implements URIResolver { diff --git a/mycore-base/src/main/resources/config/deprecated.properties b/mycore-base/src/main/resources/config/deprecated.properties index 604c0fdac8..403e1e3d41 100644 --- a/mycore-base/src/main/resources/config/deprecated.properties +++ b/mycore-base/src/main/resources/config/deprecated.properties @@ -5,3 +5,7 @@ MCR.Filter.UserAgent=MCR.Filter.UserAgent.BotPattern MCR.Jersey.resource.packages=MCR.Jersey.Resource.Packages MCR.URN.Enabled.Objects=Your.Own.Property.see.MCR-1583 + +MCR.URIResolver.MaxObjectSize=MCR.HTTPClient.MaxObjectSize +MCR.URIResolver.MaxCacheEntries=MCR.HTTPClient.MaxCacheEntries +MCR.URIResolver.RequestTimeout=MCR.HTTPClient.RequestTimeout diff --git a/mycore-base/src/main/resources/config/mycore.properties b/mycore-base/src/main/resources/config/mycore.properties index e7731b4106..c98b5b0d46 100644 --- a/mycore-base/src/main/resources/config/mycore.properties +++ b/mycore-base/src/main/resources/config/mycore.properties @@ -73,6 +73,12 @@ MCR.Filter.UserAgent.AcceptInvalid=false MCR.URIResolver.CachingResolver.Capacity=100 MCR.URIResolver.CachingResolver.MaxAge=3600000 + +# The HTTP Client in use (currently only by MCR.URIResolver.MCRRESTResolver) + MCR.HTTPClient.Class=org.mycore.common.MCRDefaultHTTPClient + MCR.HTTPClient.MaxObjectSize=131072 + MCR.HTTPClient.MaxCacheEntries=1000 + MCR.HTTPClient.RequestTimeout=30000 ############################################################################## # Classes for the commandline interface From b7261d8ead80fea04d181aba03bf0448639fd375 Mon Sep 17 00:00:00 2001 From: Thomas Scheffler Date: Thu, 15 Feb 2024 11:02:43 +0100 Subject: [PATCH 07/10] MCR-3041 rest api v 2 quirks (#2062) * Bad Request for invalid MCRObjectID e.g. api/v2/objects/xx * fix MCRObject not found catch IOException that is thrown if MCRObject does not exist * provide X-Error-* header for MCRErrorResponse allows to submit some details of the error also via HTTP header * validate derivate relation for derivate content access re-uses MCRRestDerivates.validateDerivateRelation(mcrId, derid) * fixed CORS-preflight request detection OPTIONS is not enough also Access-Control-Request-Method header is required * use MCRErrorResponse when invalid MCRObjectID --- .../org/mycore/restapi/MCRCORSResponseFilter.java | 7 ++++++- .../converter/MCRObjectIDParamConverterProvider.java | 11 +++++++---- .../java/org/mycore/restapi/v2/MCRErrorResponse.java | 5 +++++ .../mycore/restapi/v2/MCRRestAuthorizationFilter.java | 10 +++++++++- .../mycore/restapi/v2/MCRRestDerivateContents.java | 4 ++++ .../java/org/mycore/restapi/v2/MCRRestObjects.java | 11 ++++++++--- 6 files changed, 39 insertions(+), 9 deletions(-) diff --git a/mycore-restapi/src/main/java/org/mycore/restapi/MCRCORSResponseFilter.java b/mycore-restapi/src/main/java/org/mycore/restapi/MCRCORSResponseFilter.java index 49317f5806..87c7087e38 100644 --- a/mycore-restapi/src/main/java/org/mycore/restapi/MCRCORSResponseFilter.java +++ b/mycore-restapi/src/main/java/org/mycore/restapi/MCRCORSResponseFilter.java @@ -59,16 +59,21 @@ public class MCRCORSResponseFilter implements ContainerResponseFilter { private static final String ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers"; + private static final String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method"; + private static final String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers"; private static final String ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age"; + @Context ResourceInfo resourceInfo; private static boolean handlePreFlight(ContainerRequestContext requestContext, MultivaluedMap responseHeaders) { - if (!requestContext.getMethod().equals(HttpMethod.OPTIONS)) { + if (!requestContext.getMethod().equals(HttpMethod.OPTIONS) + || requestContext.getHeaderString(ACCESS_CONTROL_REQUEST_METHOD) == null) { + //required for CORS-preflight request return false; } //allow all methods diff --git a/mycore-restapi/src/main/java/org/mycore/restapi/converter/MCRObjectIDParamConverterProvider.java b/mycore-restapi/src/main/java/org/mycore/restapi/converter/MCRObjectIDParamConverterProvider.java index a449ad2830..0d65a1bc71 100644 --- a/mycore-restapi/src/main/java/org/mycore/restapi/converter/MCRObjectIDParamConverterProvider.java +++ b/mycore-restapi/src/main/java/org/mycore/restapi/converter/MCRObjectIDParamConverterProvider.java @@ -23,9 +23,9 @@ import org.mycore.common.MCRException; import org.mycore.datamodel.metadata.MCRObjectID; +import org.mycore.restapi.v2.MCRErrorResponse; import jakarta.ws.rs.BadRequestException; -import jakarta.ws.rs.core.Response; import jakarta.ws.rs.ext.ParamConverter; import jakarta.ws.rs.ext.ParamConverterProvider; import jakarta.ws.rs.ext.Provider; @@ -50,9 +50,12 @@ public T fromString(String value) { try { return rawType.cast(MCRObjectID.getInstance(value)); } catch (MCRException e) { - throw new BadRequestException(Response.status(CODE_INVALID) - .entity(MSG_INVALID) - .build()); + throw MCRErrorResponse.fromStatus(CODE_INVALID) + .withErrorCode("MCROBJECTID_INVALID") + .withMessage(MSG_INVALID) + .withDetail(e.getMessage()) + .withCause(e) + .toException(); } } diff --git a/mycore-restapi/src/main/java/org/mycore/restapi/v2/MCRErrorResponse.java b/mycore-restapi/src/main/java/org/mycore/restapi/v2/MCRErrorResponse.java index c083db5678..347df5a689 100644 --- a/mycore-restapi/src/main/java/org/mycore/restapi/v2/MCRErrorResponse.java +++ b/mycore-restapi/src/main/java/org/mycore/restapi/v2/MCRErrorResponse.java @@ -83,6 +83,11 @@ public WebApplicationException toException() { WebApplicationException e; Response.Status s = Response.Status.fromStatusCode(status); final Response response = Response.status(s) + .header("X-Error-Code", getErrorCode()) + .header("X-Error-Message", getMessage()) + .header("X-Error-Time", new Date(getTimestamp().toEpochMilli())) //no HeaderDelegate for Instant + .header("X-Error-Detail", getDetail()) + .header("X-Error-UUID", getUuid()) .entity(this) .build(); //s maybe null diff --git a/mycore-restapi/src/main/java/org/mycore/restapi/v2/MCRRestAuthorizationFilter.java b/mycore-restapi/src/main/java/org/mycore/restapi/v2/MCRRestAuthorizationFilter.java index 6dcafe6e95..0da65d14f0 100644 --- a/mycore-restapi/src/main/java/org/mycore/restapi/v2/MCRRestAuthorizationFilter.java +++ b/mycore-restapi/src/main/java/org/mycore/restapi/v2/MCRRestAuthorizationFilter.java @@ -27,8 +27,10 @@ import org.apache.logging.log4j.LogManager; import org.mycore.access.MCRAccessManager; import org.mycore.access.mcrimpl.MCRAccessControlSystem; +import org.mycore.datamodel.metadata.MCRObjectID; import org.mycore.frontend.jersey.access.MCRRequestScopeACL; import org.mycore.restapi.converter.MCRDetailLevel; +import org.mycore.restapi.converter.MCRObjectIDParamConverterProvider; import org.mycore.restapi.v2.access.MCRRestAPIACLPermission; import org.mycore.restapi.v2.access.MCRRestAccessManager; import org.mycore.restapi.v2.annotation.MCRRestRequiredPermission; @@ -44,6 +46,7 @@ import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ParamConverter; @Priority(Priorities.AUTHORIZATION) public class MCRRestAuthorizationFilter implements ContainerRequestFilter { @@ -56,6 +59,9 @@ public class MCRRestAuthorizationFilter implements ContainerRequestFilter { public static final String PARAM_DER_PATH = "path"; + public static final ParamConverter OBJECT_ID_PARAM_CONVERTER = new MCRObjectIDParamConverterProvider() + .getConverter(MCRObjectID.class, null, null); + @Context ResourceInfo resourceInfo; @@ -88,7 +94,9 @@ private void checkBaseAccess(ContainerRequestContext requestContext, MCRRestAPIA Optional checkable = Optional.ofNullable(derId) .filter(d -> path != null) //only check for derId if path is given .map(Optional::of) - .orElseGet(() -> Optional.ofNullable(objectId)); + .orElseGet(() -> Optional.ofNullable(objectId)) + .map(OBJECT_ID_PARAM_CONVERTER::fromString) //MCR-3041 check for Bad Request + .map(MCRObjectID::toString); checkable.ifPresent(id -> LogManager.getLogger().info("Checking " + permission + " access on " + id)); MCRRequestScopeACL aclProvider = MCRRequestScopeACL.getInstance(requestContext); boolean allowed = checkable diff --git a/mycore-restapi/src/main/java/org/mycore/restapi/v2/MCRRestDerivateContents.java b/mycore-restapi/src/main/java/org/mycore/restapi/v2/MCRRestDerivateContents.java index ac887cad42..580d04f552 100644 --- a/mycore-restapi/src/main/java/org/mycore/restapi/v2/MCRRestDerivateContents.java +++ b/mycore-restapi/src/main/java/org/mycore/restapi/v2/MCRRestDerivateContents.java @@ -289,6 +289,7 @@ private static byte[] getMD5Digest(String md5sum) { @Header(name = "Last-Modified", description = "last modified date of file"), })) public Response getFileOrDirectoryMetadata() { + MCRRestDerivates.validateDerivateRelation(mcrId, derid); MCRPath mcrPath = getPath(); MCRFileAttributes fileAttributes; try { @@ -329,6 +330,7 @@ public Response getFileOrDirectoryMetadata() { summary = "List directory contents or serves file given by {path} in derivate", tags = MCRRestUtils.TAG_MYCORE_FILE) public Response getFileOrDirectory(@Context UriInfo uriInfo, @Context HttpHeaders requestHeader) { + MCRRestDerivates.validateDerivateRelation(mcrId, derid); LogManager.getLogger().info("{}:{}", derid, path); MCRPath mcrPath = MCRPath.getPath(derid.toString(), path); MCRFileAttributes fileAttributes; @@ -384,6 +386,7 @@ public Response getFileOrDirectory(@Context UriInfo uriInfo, @Context HttpHeader }, tags = MCRRestUtils.TAG_MYCORE_FILE) public Response createFileOrDirectory(InputStream contents) { + MCRRestDerivates.validateDerivateRelation(mcrId, derid); MCRPath mcrPath = MCRPath.getPath(derid.toString(), path); if (mcrPath.getNameCount() > 1) { MCRPath parentDirectory = mcrPath.getParent(); @@ -429,6 +432,7 @@ public Response createFileOrDirectory(InputStream contents) { tags = MCRRestUtils.TAG_MYCORE_FILE) @MCRRequireTransaction public Response deleteFileOrDirectory() { + MCRRestDerivates.validateDerivateRelation(mcrId, derid); MCRPath mcrPath = getPath(); try { if (Files.exists(mcrPath) && Files.isDirectory(mcrPath)) { diff --git a/mycore-restapi/src/main/java/org/mycore/restapi/v2/MCRRestObjects.java b/mycore-restapi/src/main/java/org/mycore/restapi/v2/MCRRestObjects.java index 6acd70bb9a..b4bd6c4573 100644 --- a/mycore-restapi/src/main/java/org/mycore/restapi/v2/MCRRestObjects.java +++ b/mycore-restapi/src/main/java/org/mycore/restapi/v2/MCRRestObjects.java @@ -400,9 +400,14 @@ public Response createObject(String xml) { tags = MCRRestUtils.TAG_MYCORE_OBJECT) public Response getObject(@Parameter(example = "mir_mods_00004711") @PathParam(PARAM_MCRID) MCRObjectID id) throws IOException { - long modified = MCRXMLMetadataManager.instance().getLastModified(id); - if (modified < 0) { - throw new NotFoundException("MCRObject " + id + " not found"); + long modified; + try { + modified = MCRXMLMetadataManager.instance().getLastModified(id); + } catch (IOException io) { + throw MCRErrorResponse.fromStatus(Response.Status.NOT_FOUND.getStatusCode()) + .withErrorCode(MCRErrorCodeConstants.MCROBJECT_NOT_FOUND) + .withMessage("MCRObject " + id + " not found") + .toException(); } Date lastModified = new Date(modified); Optional cachedResponse = MCRRestUtils.getCachedResponse(request, lastModified); From 788a91665aa9c7938a1ce2a1fc68c494cdc448a0 Mon Sep 17 00:00:00 2001 From: Thomas Scheffler Date: Thu, 15 Feb 2024 16:02:15 +0100 Subject: [PATCH 08/10] MCR-3041 rest api v 2 quirks (#2065) * expose all X- header for CORS requests * only add WWW-Authenticate header when unauthorized --- .../java/org/mycore/restapi/MCRCORSResponseFilter.java | 4 ++++ .../main/java/org/mycore/restapi/MCRSessionFilter.java | 8 ++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/mycore-restapi/src/main/java/org/mycore/restapi/MCRCORSResponseFilter.java b/mycore-restapi/src/main/java/org/mycore/restapi/MCRCORSResponseFilter.java index 87c7087e38..bdf83745ca 100644 --- a/mycore-restapi/src/main/java/org/mycore/restapi/MCRCORSResponseFilter.java +++ b/mycore-restapi/src/main/java/org/mycore/restapi/MCRCORSResponseFilter.java @@ -111,6 +111,10 @@ public void filter(ContainerRequestContext requestContext, ContainerResponseCont if (!handlePreFlight(requestContext, responseHeaders)) { //not a CORS preflight request ArrayList exposedHeaders = new ArrayList<>(); + //MCR-3041 expose all header starting with X- + responseHeaders.keySet().stream() + .filter(name -> name.startsWith("x-") || name.startsWith("X-")) + .forEach(exposedHeaders::add); if (authenticatedRequest && responseHeaders.getFirst(HttpHeaders.AUTHORIZATION) != null) { exposedHeaders.add(HttpHeaders.AUTHORIZATION); } diff --git a/mycore-restapi/src/main/java/org/mycore/restapi/MCRSessionFilter.java b/mycore-restapi/src/main/java/org/mycore/restapi/MCRSessionFilter.java index 3dade395d2..1d93a82839 100644 --- a/mycore-restapi/src/main/java/org/mycore/restapi/MCRSessionFilter.java +++ b/mycore-restapi/src/main/java/org/mycore/restapi/MCRSessionFilter.java @@ -250,8 +250,8 @@ public void filter(ContainerRequestContext requestContext, ContainerResponseCont try { MCRSessionMgr.unlock(); MCRSession currentSession = MCRSessionMgr.getCurrentSession(); - if (responseContext.getStatus() == Response.Status.FORBIDDEN.getStatusCode() && currentSession - .getUserInformation().getUserID().equals(MCRSystemUserInformation.getGuestInstance().getUserID())) { + if (responseContext.getStatus() == Response.Status.FORBIDDEN.getStatusCode() + && isUnAuthorized(requestContext)) { LOGGER.debug("Guest detected, change response from FORBIDDEN to UNAUTHORIZED."); responseContext.setStatus(Response.Status.UNAUTHORIZED.getStatusCode()); responseContext.getHeaders().putSingle(HttpHeaders.WWW_AUTHENTICATE, @@ -285,6 +285,10 @@ public void close() throws IOException { } } + private static boolean isUnAuthorized(ContainerRequestContext requestContext) { + return requestContext.getHeaderString(HttpHeaders.AUTHORIZATION) == null; + } + //returns true for Ajax-Requests or requests for embedded images private static boolean doNotWWWAuthenticate(ContainerRequestContext requestContext) { return !"ServiceWorker".equals(requestContext.getHeaderString("X-Requested-With")) && From d8ccf6c0eae76a1b10d9ce4302467620296a3f2f Mon Sep 17 00:00:00 2001 From: Sebastian Hofmann <7668803+sebhofmann@users.noreply.github.com> Date: Tue, 20 Feb 2024 17:19:10 +0100 Subject: [PATCH 09/10] MCR-3046 allow id generation independent from actual storage (#2067) * MCR-3046 allow id generation independent from actual storage * MCR-3046 removed junit5 code * MCR-3046 some improvements * store ids as strings to make debugging easier * add a docs to setNextFreeId * add missing copyrights * MCR-3046 add javadoc * reverted unwanted change --- .../MCRFileBaseCacheObjectIDGenerator.java | 214 ++++++++++++++++++ .../frontend/cli/MCRObjectCommands.java | 15 ++ ...MCRFileBaseCacheObjectIDGeneratorTest.java | 76 +++++++ 3 files changed, 305 insertions(+) create mode 100644 mycore-base/src/main/java/org/mycore/datamodel/common/MCRFileBaseCacheObjectIDGenerator.java create mode 100644 mycore-base/src/test/java/org/mycore/datamodel/common/MCRFileBaseCacheObjectIDGeneratorTest.java diff --git a/mycore-base/src/main/java/org/mycore/datamodel/common/MCRFileBaseCacheObjectIDGenerator.java b/mycore-base/src/main/java/org/mycore/datamodel/common/MCRFileBaseCacheObjectIDGenerator.java new file mode 100644 index 0000000000..4a52c89d61 --- /dev/null +++ b/mycore-base/src/main/java/org/mycore/datamodel/common/MCRFileBaseCacheObjectIDGenerator.java @@ -0,0 +1,214 @@ +/* + * This file is part of *** M y C o R e *** + * See http://www.mycore.de/ for details. + * + * MyCoRe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * MyCoRe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MyCoRe. If not, see . + */ + +package org.mycore.datamodel.common; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.mycore.common.MCRException; +import org.mycore.common.MCRUtils; +import org.mycore.common.config.MCRConfiguration2; +import org.mycore.datamodel.metadata.MCRObjectID; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * This class generates object ids based on a file based cache. The cache is used to store the last generated id for a + * given base id. The cache file is located in the data directory of MyCoRe and is named "id_cache" and contains one + * file for each base id. The file contains the last generated id as a string. + */ +public class MCRFileBaseCacheObjectIDGenerator implements MCRObjectIDGenerator { + + private static final Logger LOGGER = LogManager.getLogger(); + + static ConcurrentHashMap locks = new ConcurrentHashMap<>(); + + private static Path getCacheFilePath(String baseId) { + + Path dataDir = getDataDirPath(); + + Path idCachePath = dataDir.resolve("id_cache"); + if (!Files.exists(idCachePath)) { + synchronized (MCRFileBaseCacheObjectIDGenerator.class) { + if (!Files.exists(idCachePath)) { + try { + Files.createDirectory(idCachePath); + } catch (IOException e) { + throw new MCRException( + "Could not create " + idCachePath.toAbsolutePath() + " directory", e); + } + } + } + } + + Path cacheFile = MCRUtils.safeResolve(idCachePath, baseId); + if (!Files.exists(cacheFile)) { + synchronized (MCRFileBaseCacheObjectIDGenerator.class) { + if (!Files.exists(cacheFile)) { + try { + Files.createFile(cacheFile); + } catch (IOException e) { + throw new MCRException("Could not create " + cacheFile.toAbsolutePath(), e); + } + } + } + } + return cacheFile; + } + + static Path getDataDirPath() { + Path path = Paths.get(MCRConfiguration2.getStringOrThrow("MCR.datadir")); + if (Files.exists(path) && !Files.isDirectory(path)) { + throw new MCRException("Data directory does not exist or is not a directory: " + path); + } + return path; + } + + private static void writeNewID(MCRObjectID nextID, ByteBuffer buffer, FileChannel channel, Path cacheFile) + throws IOException { + buffer.clear(); + channel.position(0); + byte[] idAsBytes = nextID.toString().getBytes(StandardCharsets.UTF_8); + buffer.put(idAsBytes); + buffer.flip(); + int written = channel.write(buffer); + if (written != idAsBytes.length) { + throw new MCRException("Could not write new ID to " + cacheFile.toAbsolutePath()); + } + } + + /** + * Set the next free id for the given baseId. Should only be used for migration purposes and the caller has to make + * sure that the cache file is not used by another process. + * @param baseId the base id + * @param next the next free id to be returned by getNextFreeId + */ + public void setNextFreeId(String baseId, int next) { + Path cacheFile = getCacheFilePath(baseId); + + int idLengthInBytes = MCRObjectID.formatID(baseId, 1).getBytes(StandardCharsets.UTF_8).length; + try ( + FileChannel channel = FileChannel.open(cacheFile, StandardOpenOption.WRITE, + StandardOpenOption.SYNC, StandardOpenOption.CREATE);){ + ByteBuffer buffer = ByteBuffer.allocate(idLengthInBytes); + channel.position(0); + writeNewID(MCRObjectID.getInstance(MCRObjectID.formatID(baseId, next-1)), buffer, channel, cacheFile); + } catch (FileNotFoundException e) { + throw new MCRException("Could not create " + cacheFile.toAbsolutePath(), e); + } catch (IOException e) { + throw new MCRException("Could not open " + cacheFile.toAbsolutePath(), e); + } + } + + @Override + public MCRObjectID getNextFreeId(String baseId, int maxInWorkflow) { + Path cacheFile = getCacheFilePath(baseId); + + MCRObjectID nextID; + + ReentrantReadWriteLock lock = locks.computeIfAbsent(baseId, k -> new ReentrantReadWriteLock()); + ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); + + try { + writeLock.lock(); + try ( + FileChannel channel = FileChannel.open(cacheFile, StandardOpenOption.READ, StandardOpenOption.WRITE, + StandardOpenOption.SYNC); + FileLock fileLock = channel.lock()) { + + int idLengthInBytes = MCRObjectID.formatID(baseId, 1).getBytes(StandardCharsets.UTF_8).length; + ByteBuffer buffer = ByteBuffer.allocate(idLengthInBytes); + buffer.clear(); + channel.position(0); + int bytesRead = channel.read(buffer); + if (bytesRead <= 0) { + LOGGER.info("No ID found in " + cacheFile.toAbsolutePath()); + // empty file -> new currentID is 1 + nextID = MCRObjectID.getInstance(MCRObjectID.formatID(baseId, maxInWorkflow + 1)); + writeNewID(nextID, buffer, channel, cacheFile); + } else if (bytesRead == idLengthInBytes) { + buffer.flip(); + MCRObjectID objectID = readObjectIDFromBuffer(idLengthInBytes, buffer); + int lastID = objectID.getNumberAsInteger(); + nextID = MCRObjectID.getInstance(MCRObjectID.formatID(baseId, lastID + maxInWorkflow + 1)); + writeNewID(nextID, buffer, channel, cacheFile); + } else { + throw new MCRException("Content has different id length " + cacheFile.toAbsolutePath()); + } + } catch (FileNotFoundException e) { + throw new MCRException("Could not create " + cacheFile.toAbsolutePath(), e); + } catch (IOException e) { + throw new MCRException("Could not open " + cacheFile.toAbsolutePath(), e); + } + } finally { + writeLock.unlock(); + } + + return nextID; + } + + private static MCRObjectID readObjectIDFromBuffer(int idLengthBytes, ByteBuffer buffer) { + byte[] idBytes = new byte[idLengthBytes]; + buffer.get(idBytes); + String lastIDString = new String(idBytes, StandardCharsets.UTF_8); + return MCRObjectID.getInstance(lastIDString); + } + + @Override + public MCRObjectID getLastID(String baseId) { + Path cacheFilePath = getCacheFilePath(baseId); + ReentrantReadWriteLock lock = locks.computeIfAbsent(baseId, k -> new ReentrantReadWriteLock()); + ReentrantReadWriteLock.ReadLock readLock = lock.readLock(); + try { + readLock.lock(); + int idLengthInBytes = MCRObjectID.formatID(baseId, 1).getBytes(StandardCharsets.UTF_8).length; + + try (FileChannel channel = FileChannel.open(cacheFilePath)) { + ByteBuffer buffer = ByteBuffer.allocate(idLengthInBytes); + buffer.clear(); + channel.position(0); + int bytesRead = channel.read(buffer); + if (bytesRead == -1) { + // empty file -> no ID found + return null; + } else if (bytesRead == idLengthInBytes) { + buffer.flip(); + return readObjectIDFromBuffer(idLengthInBytes, buffer); + } else { + throw new MCRException("Content has different id length " + cacheFilePath.toAbsolutePath()); + } + } catch (IOException e) { + throw new MCRException("Could not open " + cacheFilePath.toAbsolutePath(), e); + } + } finally { + readLock.unlock(); + } + } + +} diff --git a/mycore-base/src/main/java/org/mycore/frontend/cli/MCRObjectCommands.java b/mycore-base/src/main/java/org/mycore/frontend/cli/MCRObjectCommands.java index 0eea4718a2..35f6e304fd 100644 --- a/mycore-base/src/main/java/org/mycore/frontend/cli/MCRObjectCommands.java +++ b/mycore-base/src/main/java/org/mycore/frontend/cli/MCRObjectCommands.java @@ -84,6 +84,7 @@ import org.mycore.common.xsl.MCRErrorListener; import org.mycore.datamodel.common.MCRAbstractMetadataVersion; import org.mycore.datamodel.common.MCRActiveLinkException; +import org.mycore.datamodel.common.MCRFileBaseCacheObjectIDGenerator; import org.mycore.datamodel.common.MCRLinkTableManager; import org.mycore.datamodel.common.MCRXMLMetadataManager; import org.mycore.datamodel.metadata.MCRBase; @@ -1386,6 +1387,20 @@ public static void repairSharedMetadata(String id) throws MCRAccessException { MCRMetadataManager.repairSharedMetadata(obj); } + @MCRCommand( + syntax = "create object id cache", + help = "Creates a cache for all object ids in the configuration directory.", + order = 175) + public static void createObjectIDCache() { + MCRXMLMetadataManager metadataManager = MCRXMLMetadataManager.instance(); + metadataManager.getObjectBaseIds().forEach(id -> { + LOGGER.info("Creating cache for base {}", id); + int highestStoredID = metadataManager.getHighestStoredID(id); + MCRFileBaseCacheObjectIDGenerator gen = new MCRFileBaseCacheObjectIDGenerator(); + gen.setNextFreeId(id, highestStoredID+1); + }); + } + /** * The method start the repair of the metadata search for a given MCRObjectID as String. * diff --git a/mycore-base/src/test/java/org/mycore/datamodel/common/MCRFileBaseCacheObjectIDGeneratorTest.java b/mycore-base/src/test/java/org/mycore/datamodel/common/MCRFileBaseCacheObjectIDGeneratorTest.java new file mode 100644 index 0000000000..d18c5e0d60 --- /dev/null +++ b/mycore-base/src/test/java/org/mycore/datamodel/common/MCRFileBaseCacheObjectIDGeneratorTest.java @@ -0,0 +1,76 @@ +/* + * This file is part of *** M y C o R e *** + * See http://www.mycore.de/ for details. + * + * MyCoRe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * MyCoRe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MyCoRe. If not, see . + */ + +package org.mycore.datamodel.common; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.junit.Test; +import org.mycore.common.MCRTestCase; +import org.mycore.datamodel.metadata.MCRObjectID; + +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Collections; +import java.util.stream.IntStream; + +import static org.junit.Assert.assertEquals; + +public class MCRFileBaseCacheObjectIDGeneratorTest extends MCRTestCase { + + public static final int GENERATOR_COUNT = 10; + public static final int TEST_IDS = 100; + + private static final Logger LOGGER = LogManager.getLogger(); + + @Test + public void getNextFreeId() throws IOException { + Files.createDirectories(MCRFileBaseCacheObjectIDGenerator.getDataDirPath()); + + var generatorList = new ArrayList(); + for (int i = 0; i < GENERATOR_COUNT; i++) { + generatorList.add(new MCRFileBaseCacheObjectIDGenerator()); + } + + // need thread safe list of generated ids + var generatedIds = Collections.synchronizedList(new ArrayList()); + IntStream.range(0, TEST_IDS) + .parallel() + .forEach(i -> { + LOGGER.info("Generating ID {}", i); + var generator = generatorList.get(i % GENERATOR_COUNT); + MCRObjectID id = generator.getNextFreeId("junit", "test"); + generatedIds.add(id); + }); + + + // check if all ids are unique + assertEquals(TEST_IDS, generatedIds.size()); + assertEquals(TEST_IDS, generatedIds.stream().distinct().count()); + + // check if there is no space in the ids + var sortedIds = new ArrayList<>(generatedIds); + Collections.sort(sortedIds); + for (int i = 0; i < sortedIds.size() - 1; i++) { + assertEquals(i+1, sortedIds.get(i).getNumberAsInteger()); + } + + } + +} From 2135439ac9d2fcd58483b2656bcfd01701b47f8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Adler?= Date: Tue, 5 Dec 2023 09:35:18 +0100 Subject: [PATCH 10/10] MCR-3011 fix duplicate dateModified --- mycore-mods/src/main/resources/xsl/mods2schemaorg.xsl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mycore-mods/src/main/resources/xsl/mods2schemaorg.xsl b/mycore-mods/src/main/resources/xsl/mods2schemaorg.xsl index 5fb6f43969..8782422281 100644 --- a/mycore-mods/src/main/resources/xsl/mods2schemaorg.xsl +++ b/mycore-mods/src/main/resources/xsl/mods2schemaorg.xsl @@ -205,10 +205,10 @@ - - + + + select="mods:originInfo[@eventType='creation']/mods:dateCreated[@encoding='w3cdtf' and not(@type)]" />