diff --git a/.travis.yml b/.travis.yml index 66268894..212b56ab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -45,38 +45,36 @@ jobs: before_script: travis_wait bash _ci/cache_artifacts.sh install: travis_wait 20 mvn -B -U -q clean install -DadditionalOption=-Xdoclint:none -DskipTests -Dmaven.javadoc.skip=true -Dmaven.wagon.http.pool=false -Pbase script: mvn -B -U clean deploy -DadditionalOption=-Xdoclint:none -Dmaven.javadoc.skip=true -Dmaven.wagon.http.pool=false -Pbase + - name: "ImageMagick" before_script: travis_wait bash _ci/cache_artifacts.sh install: _ci/build.sh imagemagick script: bash _ci/test.sh imagemagick + - name: "LibreOffice" before_script: travis_wait bash _ci/cache_artifacts.sh install: _ci/build.sh libreoffice script: bash _ci/test.sh libreoffice + - name: "Transform Misc" before_script: travis_wait bash _ci/cache_artifacts.sh install: _ci/build.sh misc script: bash _ci/test.sh misc + - name: "PDF Renderer" before_script: travis_wait bash _ci/cache_artifacts.sh install: _ci/build.sh pdf-renderer script: bash _ci/test.sh pdf-renderer + - name: "Tika" before_script: travis_wait bash _ci/cache_artifacts.sh install: _ci/build.sh tika script: bash _ci/test.sh tika + - name: "All in One Transformer" before_script: travis_wait bash _ci/cache_artifacts.sh install: _ci/build.sh full-build script: travis_wait 30 bash _ci/test.sh aio-test -# - name: "Static Analysis (SAST)" -# TODO ATS-721: comment out until it is possible to run concurrent SAST scans -# if: type != pull_request -# if: branch = master AND type != pull_request -# before_install: -# - bash _ci/static_analysis_init.sh -# - bash _ci/init.sh -# script: bash _ci/static_analysis.sh - name: "Release" stage: release diff --git a/README.md b/README.md index 4723c2b2..32cb9a5d 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,16 @@ ## Alfresco Transform Core [![Build Status](https://travis-ci.com/Alfresco/alfresco-transform-core.svg?branch=master)](https://travis-ci.com/Alfresco/alfresco-transform-core) -Contains the common transformer (T-Engine) code, plus a few actual implementations. +Contains the common transformer (T-Engine) code, plus a few implementations. ### Sub-projects +* `alfresco-transform-model` - library packaged as a jar file which contains the data model of json + configuration files and messages sent between clients, T-Engines and T-Router. Also contains code to + work out which transform should be used for a combination of configuration files; see the sub-project's + [README](https://github.com/Alfresco/alfresco-transform-core/blob/master/alfresco-transform-model/README.md) * `alfresco-transformer-base` - library packaged as a jar file which contains code that is common - to all the transformers; see the sub-project's + to all the transformers; see the sub-project's [README](https://github.com/Alfresco/alfresco-transform-core/blob/master/alfresco-transformer-base/README.md) * `alfresco-transform-` - multiple T-Engines; each one of them builds both a SpringBoot fat jar and a [Docker image](https://github.com/Alfresco/alfresco-transform-core#docker) @@ -37,6 +41,12 @@ The artifacts can be obtained by: * downloading from [Alfresco repository](https://artifacts.alfresco.com/nexus/content/groups/public) * getting as Maven dependency by adding the dependency to your pom file: ```xml + + org.alfresco + alfresco-transform-model + version + + org.alfresco alfresco-transformer-base diff --git a/_ci/static_analysis.sh b/_ci/static_analysis.sh deleted file mode 100644 index 05818f36..00000000 --- a/_ci/static_analysis.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash - -echo "=========================== Starting Static Analysis Script ===========================" -PS4="\[\e[35m\]+ \[\e[m\]" -set -vex -pushd "$(dirname "${BASH_SOURCE[0]}")/../" - -# Run in a sandbox for every branch, run normally on master -[ "${TRAVIS_BRANCH}" != "master" ] && RUN_IN_SANDBOX="-sandboxname Transformers" || RUN_IN_SANDBOX="" - -java -jar vosp-api-wrappers-java-$VERACODE_WRAPPER_VERSION.jar -vid $VERACODE_API_ID \ - -vkey $VERACODE_API_KEY -action uploadandscan -appname "Transform Service" \ - ${RUN_IN_SANDBOX} -createprofile false \ - -filepath \ - alfresco-transform-core-aio/alfresco-transform-core-aio-boot/target/alfresco-transform-core-aio-boot*.jar \ - -version "$TRAVIS_JOB_ID - $TRAVIS_JOB_NUMBER" -scantimeout 3600 - -popd -set +vex -echo "=========================== Finishing Static Analysis Script ==========================" diff --git a/_ci/static_analysis_init.sh b/_ci/static_analysis_init.sh deleted file mode 100644 index 6f649ca2..00000000 --- a/_ci/static_analysis_init.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash - -echo "=========================== Starting Static Analysis Init Script ===========================" -PS4="\[\e[35m\]+ \[\e[m\]" -set -vex -pushd "$(dirname "${BASH_SOURCE[0]}")/../" - -wget https://repo1.maven.org/maven2/com/veracode/vosp/api/wrappers/vosp-api-wrappers-java/$VERACODE_WRAPPER_VERSION/vosp-api-wrappers-java-$VERACODE_WRAPPER_VERSION.jar -sha1sum -c <<< "$VERACODE_WRAPPER_SHA1 vosp-api-wrappers-java-$VERACODE_WRAPPER_VERSION.jar" - -popd -set +vex -echo "=========================== Finishing Static Analysis Init Script ==========================" \ No newline at end of file diff --git a/alfresco-transform-model/README.md b/alfresco-transform-model/README.md new file mode 100644 index 00000000..a3352263 --- /dev/null +++ b/alfresco-transform-model/README.md @@ -0,0 +1,4 @@ +# alfresco-transform-model +Alfresco Transform Model - Contains the data model of json configuration files +and messages sent between clients, T-Engines and T-Router. Also contains code to +work out which transform should be used for a combination of configuration files. \ No newline at end of file diff --git a/alfresco-transform-model/pom.xml b/alfresco-transform-model/pom.xml new file mode 100644 index 00000000..9daa9873 --- /dev/null +++ b/alfresco-transform-model/pom.xml @@ -0,0 +1,152 @@ + + + 4.0.0 + Alfresco Transform Model + org.alfresco + alfresco-transform-model + + + org.alfresco + alfresco-transform-core + 2.6.0-A2-SNAPSHOT + + + + UTF-8 + UTF-8 + + + + + org.springframework.boot + spring-boot-starter-web + provided + + + org.apache.commons + commons-lang3 + provided + + + org.apache.maven + maven-artifact + 3.8.5 + + + com.fasterxml.jackson.core + jackson-annotations + provided + + + junit + junit + test + + + org.junit.jupiter + junit-jupiter + test + + + org.mockito + mockito-junit-jupiter + test + + + com.google.guava + guava + test + + + + + + + + maven-release-plugin + 2.5.3 + + @{project.version} + + + + org.apache.maven.plugins + maven-surefire-plugin + + 1 + true + + + + default-test + + test + + test + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + 1 + true + + + + + integration-test + verify + + + + + + org.codehaus.mojo + license-maven-plugin + 2.0.1.alfresco-2 + + + third-party-licenses + + add-third-party + download-licenses + + generate-resources + + true + provided,test + org.alfresco + true + https://raw.githubusercontent.com/Alfresco/third-party-license-overrides/master/includedLicenses.txt + https://raw.githubusercontent.com/Alfresco/third-party-license-overrides/master/licenseMerges.txt + https://raw.githubusercontent.com/Alfresco/third-party-license-overrides/master/override-THIRD-PARTY.properties + + + + + + + + + + org.codehaus.mojo + license-maven-plugin + + + org.apache.maven.plugins + maven-jar-plugin + 3.2.2 + + + + test-jar + + + + + + + + diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/InternalContext.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/InternalContext.java new file mode 100644 index 00000000..eb9b0b79 --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/InternalContext.java @@ -0,0 +1,174 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.model; + +import org.alfresco.transform.router.TransformStack; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +/** + * Holds required contextual information. + * + * @author Denis Ungureanu + * created on 10/01/2019 + */ +public class InternalContext implements Serializable +{ + private MultiStep multiStep; + private int attemptedRetries; + private String currentSourceMediaType; + private String currentTargetMediaType; + private String replyToDestination; + private Long currentSourceSize; + private Map transformRequestOptions = new HashMap<>(); + + public MultiStep getMultiStep() + { + return multiStep; + } + + public void setMultiStep(MultiStep multiStep) + { + this.multiStep = multiStep; + } + + public int getAttemptedRetries() + { + return attemptedRetries; + } + + public void setAttemptedRetries(int attemptedRetries) + { + this.attemptedRetries = attemptedRetries; + } + + public String getCurrentSourceMediaType() + { + return currentSourceMediaType; + } + + public void setCurrentSourceMediaType(String currentSourceMediaType) + { + this.currentSourceMediaType = currentSourceMediaType; + } + + public String getCurrentTargetMediaType() + { + return currentTargetMediaType; + } + + public void setCurrentTargetMediaType(String currentTargetMediaType) + { + this.currentTargetMediaType = currentTargetMediaType; + } + + /** + * Gets the reply to destination name. + * + * @return replyToDestination + */ + public String getReplyToDestination() + { + return replyToDestination; + } + + /** + * Sets the reply to destination name. + * Note: replyToDestination is populated from jmsMessage replyTo field sent by T-Client + * + * @param replyToDestination reply to destination name + */ + public void setReplyToDestination(String replyToDestination) + { + this.replyToDestination = replyToDestination; + } + + public Long getCurrentSourceSize() + { + return currentSourceSize; + } + + public void setCurrentSourceSize(Long currentSourceSize) + { + this.currentSourceSize = currentSourceSize; + } + + public Map getTransformRequestOptions() + { + return transformRequestOptions; + } + + public void setTransformRequestOptions( + Map transformRequestOptions) + { + this.transformRequestOptions = transformRequestOptions; + } + + @Override public String toString() + { + return "InternalContext{" + + "multiStep=" + multiStep + + ", attemptedRetries=" + attemptedRetries + + ", currentSourceMediaType='" + currentSourceMediaType + '\'' + + ", currentTargetMediaType='" + currentTargetMediaType + '\'' + + ", replyToDestination='" + replyToDestination + '\'' + + ", currentSourceSize=" + currentSourceSize + + ", transformRequestOptions=" + transformRequestOptions + + '}'; + } + + // To avoid NPE checks, initialise expected bits of the structure + public static InternalContext initialise(InternalContext internalContext) + { + if (internalContext == null) + { + internalContext = new InternalContext(); + } + if (internalContext.getMultiStep() == null) + { + internalContext.setMultiStep(new MultiStep()); + } + if (internalContext.getMultiStep().getTransformsToBeDone() == null || + internalContext.getMultiStep().getTransformsToBeDone().isEmpty()) // might be immutable + { + internalContext.getMultiStep().setTransformsToBeDone(new ArrayList<>()); + } + return internalContext; + } + + // To avoid additional checks + public static String checkForBasicErrors(InternalContext internalContext, String type) + { + return internalContext == null + ? type + " InternalContext was null" + : internalContext.getMultiStep() == null + ? type + " InternalContext did not have the MultiStep set" + : internalContext.getMultiStep().getTransformsToBeDone() == null + ? type + " InternalContext did not have the TransformsToBeDone set" + : internalContext.getMultiStep().getInitialRequestId() == null + ? type + " InternalContext did not have the InitialRequestId set" + : TransformStack.checkStructure(internalContext, type); + } +} diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/Mimetype.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/Mimetype.java new file mode 100644 index 00000000..ce38c380 --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/Mimetype.java @@ -0,0 +1,364 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2019 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.model; + +import static java.util.Arrays.stream; +import static java.util.Collections.unmodifiableSet; +import static java.util.stream.Collectors.toSet; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Objects; +import java.util.Set; + +/** + * Mimetype + *

+ * The same mimetypes defined in org.alfresco.repo.content.MimetypeMap + */ +public class Mimetype +{ + //region Prefixes + public static final String PREFIX_APPLICATION = "application/"; + + public static final String PREFIX_AUDIO = "audio/"; + + public static final String PREFIX_IMAGE = "image/"; + + public static final String PREFIX_MESSAGE = "message/"; + + public static final String PREFIX_MODEL = "model/"; + + public static final String PREFIX_MULTIPART = "multipart/"; + + public static final String PREFIX_TEXT = "text/"; + + public static final String PREFIX_VIDEO = "video/"; + + public static final String EXTENSION_BINARY = "bin"; + + public static final String MACOS_RESOURCE_FORK_FILE_NAME_PREFIX = "._"; + //endregion + + //region Misc + public static final String MIMETYPE_MULTIPART_ALTERNATIVE = "multipart/alternative"; + + public static final String MIMETYPE_TEXT_PLAIN = "text/plain"; + + public static final String MIMETYPE_TEXT_MEDIAWIKI = "text/mediawiki"; + + public static final String MIMETYPE_TEXT_CSS = "text/css"; + + public static final String MIMETYPE_TEXT_CSV = "text/csv"; + + public static final String MIMETYPE_TEXT_JAVASCRIPT = "text/javascript"; + + public static final String MIMETYPE_XML = "text/xml"; + + public static final String MIMETYPE_HTML = "text/html"; + + public static final String MIMETYPE_XHTML = "application/xhtml+xml"; + + public static final String MIMETYPE_PDF = "application/pdf"; + + public static final String MIMETYPE_JSON = "application/json"; + + public static final String MIMETYPE_WORD = "application/msword"; + + public static final String MIMETYPE_EXCEL = "application/vnd.ms-excel"; + + public static final String MIMETYPE_BINARY = "application/octet-stream"; + + public static final String MIMETYPE_PPT = "application/vnd.ms-powerpoint"; + + public static final String MIMETYPE_APP_DWG = "application/dwg"; + + public static final String MIMETYPE_IMG_DWG = "image/vnd.dwg"; + + public static final String MIMETYPE_VIDEO_AVI = "video/x-msvideo"; + + public static final String MIMETYPE_VIDEO_QUICKTIME = "video/quicktime"; + + public static final String MIMETYPE_VIDEO_WMV = "video/x-ms-wmv"; + + public static final String MIMETYPE_VIDEO_3GP = "video/3gpp"; + + public static final String MIMETYPE_VIDEO_3GP2 = "video/3gpp2"; + + public static final String MIMETYPE_DITA = "application/dita+xml"; + //endregion + + //region Flash + public static final String MIMETYPE_FLASH = "application/x-shockwave-flash"; + + public static final String MIMETYPE_VIDEO_FLV = "video/x-flv"; + + public static final String MIMETYPE_APPLICATION_FLA = "application/x-fla"; + + public static final String MIMETYPE_VIDEO_MPG = "video/mpeg"; + + public static final String MIMETYPE_VIDEO_MP4 = "video/mp4"; + + public static final String MIMETYPE_IMAGE_GIF = "image/gif"; + + public static final String MIMETYPE_IMAGE_JPEG = "image/jpeg"; + + public static final String MIMETYPE_IMAGE_RGB = "image/x-rgb"; + + public static final String MIMETYPE_IMAGE_SVG = "image/svg+xml"; + + public static final String MIMETYPE_IMAGE_PNG = "image/png"; + + public static final String MIMETYPE_IMAGE_TIFF = "image/tiff"; + + public static final String MIMETYPE_IMAGE_RAW_DNG = "image/x-raw-adobe"; + + public static final String MIMETYPE_IMAGE_RAW_3FR = "image/x-raw-hasselblad"; + + public static final String MIMETYPE_IMAGE_RAW_RAF = "image/x-raw-fuji"; + + public static final String MIMETYPE_IMAGE_RAW_CR2 = "image/x-raw-canon"; + + public static final String MIMETYPE_IMAGE_RAW_K25 = "image/x-raw-kodak"; + + public static final String MIMETYPE_IMAGE_RAW_MRW = "image/x-raw-minolta"; + + public static final String MIMETYPE_IMAGE_RAW_NEF = "image/x-raw-nikon"; + + public static final String MIMETYPE_IMAGE_RAW_ORF = "image/x-raw-olympus"; + + public static final String MIMETYPE_IMAGE_RAW_PEF = "image/x-raw-pentax"; + + public static final String MIMETYPE_IMAGE_RAW_ARW = "image/x-raw-sony"; + + public static final String MIMETYPE_IMAGE_RAW_X3F = "image/x-raw-sigma"; + + public static final String MIMETYPE_IMAGE_RAW_RW2 = "image/x-raw-panasonic"; + + public static final String MIMETYPE_IMAGE_RAW_RWL = "image/x-raw-leica"; + + public static final String MIMETYPE_IMAGE_RAW_R3D = "image/x-raw-red"; + + public static final String MIMETYPE_IMAGE_DWT = "image/x-dwt"; + + public static final String MIMETYPE_APPLICATION_EPS = "application/eps"; + + public static final String MIMETYPE_APPLICATION_PS = "application/postscript"; + + public static final String MIMETYPE_JAVASCRIPT = "application/x-javascript"; + + public static final String MIMETYPE_ZIP = "application/zip"; + + public static final String MIMETYPE_OPENSEARCH_DESCRIPTION = "application/opensearchdescription+xml"; + + public static final String MIMETYPE_ATOM = "application/atom+xml"; + + public static final String MIMETYPE_RSS = "application/rss+xml"; + + public static final String MIMETYPE_RFC822 = "message/rfc822"; + + public static final String MIMETYPE_OUTLOOK_MSG = "application/vnd.ms-outlook"; + + public static final String MIMETYPE_VISIO = "application/vnd.visio"; + + public static final String MIMETYPE_VISIO_2013 = "application/vnd.visio2013"; + //endregion + + //region Adobe + public static final String MIMETYPE_APPLICATION_ILLUSTRATOR = "application/illustrator"; + + public static final String MIMETYPE_APPLICATION_PHOTOSHOP = "image/vnd.adobe.photoshop"; + //endregion + + //region Encrypted office document + public static final String MIMETYPE_ENCRYPTED_OFFICE = "application/x-tika-ooxml-protected"; + //endregion + + //region Open Document + public static final String MIMETYPE_OPENDOCUMENT_TEXT = "application/vnd.oasis.opendocument.text"; + + public static final String MIMETYPE_OPENDOCUMENT_TEXT_TEMPLATE = "application/vnd.oasis.opendocument.text-template"; + + public static final String MIMETYPE_OPENDOCUMENT_GRAPHICS = "application/vnd.oasis.opendocument.graphics"; + + public static final String MIMETYPE_OPENDOCUMENT_GRAPHICS_TEMPLATE = "application/vnd.oasis.opendocument.graphics-template"; + + public static final String MIMETYPE_OPENDOCUMENT_PRESENTATION = "application/vnd.oasis.opendocument.presentation"; + + public static final String MIMETYPE_OPENDOCUMENT_PRESENTATION_TEMPLATE = "application/vnd.oasis.opendocument.presentation-template"; + + public static final String MIMETYPE_OPENDOCUMENT_SPREADSHEET = "application/vnd.oasis.opendocument.spreadsheet"; + + public static final String MIMETYPE_OPENDOCUMENT_SPREADSHEET_TEMPLATE = "application/vnd.oasis.opendocument.spreadsheet-template"; + + public static final String MIMETYPE_OPENDOCUMENT_CHART = "application/vnd.oasis.opendocument.chart"; + + public static final String MIMETYPE_OPENDOCUMENT_CHART_TEMPLATE = "applicationvnd.oasis.opendocument.chart-template"; + + public static final String MIMETYPE_OPENDOCUMENT_IMAGE = "application/vnd.oasis.opendocument.image"; + + public static final String MIMETYPE_OPENDOCUMENT_IMAGE_TEMPLATE = "applicationvnd.oasis.opendocument.image-template"; + + public static final String MIMETYPE_OPENDOCUMENT_FORMULA = "application/vnd.oasis.opendocument.formula"; + + public static final String MIMETYPE_OPENDOCUMENT_FORMULA_TEMPLATE = "applicationvnd.oasis.opendocument.formula-template"; + + public static final String MIMETYPE_OPENDOCUMENT_TEXT_MASTER = "application/vnd.oasis.opendocument.text-master"; + + public static final String MIMETYPE_OPENDOCUMENT_TEXT_WEB = "application/vnd.oasis.opendocument.text-web"; + + public static final String MIMETYPE_OPENDOCUMENT_DATABASE = "application/vnd.oasis.opendocument.database"; + //endregion + + //region Open Office + public static final String MIMETYPE_OPENOFFICE1_WRITER = "application/vnd.sun.xml.writer"; + + public static final String MIMETYPE_OPENOFFICE1_CALC = "application/vnd.sun.xml.calc"; + + public static final String MIMETYPE_OPENOFFICE1_DRAW = "application/vnd.sun.xml.draw"; + + public static final String MIMETYPE_OPENOFFICE1_IMPRESS = "application/vnd.sun.xml.impress"; + //endregion + + //region Open XML + public static final String MIMETYPE_OPENXML_WORDPROCESSING = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; + public static final String MIMETYPE_OPENXML_WORDPROCESSING_MACRO = "application/vnd.ms-word.document.macroenabled.12"; + public static final String MIMETYPE_OPENXML_WORD_TEMPLATE = "application/vnd.openxmlformats-officedocument.wordprocessingml.template"; + public static final String MIMETYPE_OPENXML_WORD_TEMPLATE_MACRO = "application/vnd.ms-word.template.macroenabled.12"; + public static final String MIMETYPE_OPENXML_SPREADSHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; + public static final String MIMETYPE_OPENXML_SPREADSHEET_TEMPLATE = "application/vnd.openxmlformats-officedocument.spreadsheetml.template"; + public static final String MIMETYPE_OPENXML_SPREADSHEET_MACRO = "application/vnd.ms-excel.sheet.macroenabled.12"; + public static final String MIMETYPE_OPENXML_SPREADSHEET_TEMPLATE_MACRO = "application/vnd.ms-excel.template.macroenabled.12"; + public static final String MIMETYPE_OPENXML_SPREADSHEET_ADDIN_MACRO = "application/vnd.ms-excel.addin.macroenabled.12"; + public static final String MIMETYPE_OPENXML_SPREADSHEET_BINARY_MACRO = "application/vnd.ms-excel.sheet.binary.macroenabled.12"; + public static final String MIMETYPE_OPENXML_PRESENTATION = "application/vnd.openxmlformats-officedocument.presentationml.presentation"; + public static final String MIMETYPE_OPENXML_PRESENTATION_MACRO = "application/vnd.ms-powerpoint.presentation.macroenabled.12"; + public static final String MIMETYPE_OPENXML_PRESENTATION_SLIDESHOW = "application/vnd.openxmlformats-officedocument.presentationml.slideshow"; + public static final String MIMETYPE_OPENXML_PRESENTATION_SLIDESHOW_MACRO = "application/vnd.ms-powerpoint.slideshow.macroenabled.12"; + public static final String MIMETYPE_OPENXML_PRESENTATION_TEMPLATE = "application/vnd.openxmlformats-officedocument.presentationml.template"; + public static final String MIMETYPE_OPENXML_PRESENTATION_TEMPLATE_MACRO = "application/vnd.ms-powerpoint.template.macroenabled.12"; + public static final String MIMETYPE_OPENXML_PRESENTATION_ADDIN = "application/vnd.ms-powerpoint.addin.macroenabled.12"; + public static final String MIMETYPE_OPENXML_PRESENTATION_SLIDE = "application/vnd.openxmlformats-officedocument.presentationml.slide"; + public static final String MIMETYPE_OPENXML_PRESENTATION_SLIDE_MACRO = "application/vnd.ms-powerpoint.slide.macroenabled.12"; + //endregion + + //region Star Office + public static final String MIMETYPE_STAROFFICE5_DRAW = "application/vnd.stardivision.draw"; + + public static final String MIMETYPE_STAROFFICE5_CALC = "application/vnd.stardivision.calc"; + + public static final String MIMETYPE_STAROFFICE5_IMPRESS = "application/vnd.stardivision.impress"; + + public static final String MIMETYPE_STAROFFICE5_IMPRESS_PACKED = "application/vnd.stardivision.impress-packed"; + + public static final String MIMETYPE_STAROFFICE5_CHART = "application/vnd.stardivision.chart"; + + public static final String MIMETYPE_STAROFFICE5_WRITER = "application/vnd.stardivision.writer"; + + public static final String MIMETYPE_STAROFFICE5_WRITER_GLOBAL = "application/vnd.stardivision.writer-global"; + + public static final String MIMETYPE_STAROFFICE5_MATH = "application/vnd.stardivision.math"; + //endregion + + //region Apple iWorks + public static final String MIMETYPE_IWORK_KEYNOTE = "application/vnd.apple.keynote"; + + public static final String MIMETYPE_IWORK_NUMBERS = "application/vnd.apple.numbers"; + + public static final String MIMETYPE_IWORK_PAGES = "application/vnd.apple.pages"; + //endregion + + //region MACOS + public static final String MIMETYPE_APPLEFILE = "application/applefile"; + //endregion + + //region WordPerfect + public static final String MIMETYPE_WORDPERFECT = "application/wordperfect"; + //endregion + + //region Audio + public static final String MIMETYPE_MP3 = "audio/mpeg"; + + public static final String MIMETYPE_AUDIO_MP4 = "audio/mp4"; + + public static final String MIMETYPE_VORBIS = "audio/vorbis"; + + public static final String MIMETYPE_FLAC = "audio/x-flac"; + //endregion + + //region Alfresco + public static final String MIMETYPE_ACP = "application/acp"; + + //region other + public static final String MIMETYPE_PBM = "image/x-portable-bitmap"; + + public static final String MIMETYPE_PNM = "image/x-portable-anymap"; + + public static final String MIMETYPE_XBM = "image/x-xbitmap"; + + public static final String MIMETYPE_XPM = "image/x-xpixmap"; + + public static final String MIMETYPE_Z = "application/x-compress"; + + public static final String MIMETYPE_PPM = "image/x-portable-pixmap"; + + public static final String MIMETYPE_TAR = "application/x-tar"; + + public static final String MIMETYPE_OGG = "application/ogg"; + //endregion + + private static final Set ALL_MIMETYPES; + + static + { + ALL_MIMETYPES = unmodifiableSet(stream(Mimetype.class.getDeclaredFields()) + .filter(f -> Modifier.isPublic(f.getModifiers())) + .filter(f -> Modifier.isStatic(f.getModifiers())) + .filter(f -> Modifier.isFinal(f.getModifiers())) + .filter(f -> f.getType().isAssignableFrom(String.class)) + .filter(f -> f.getName().startsWith("MIMETYPE_")) + .peek(f -> f.setAccessible(true)) + .map(Mimetype::getFieldValue) + .filter(Objects::nonNull) + .collect(toSet())); + } + + private static String getFieldValue(final Field f) + { + try + { + return (String) f.get(null); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + + public static Set matchMimetypes(final String regex) + { + return unmodifiableSet(ALL_MIMETYPES + .stream() + .filter(t -> t.matches(regex)) + .collect(toSet())); + } +} diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/MultiStep.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/MultiStep.java new file mode 100644 index 00000000..028ab7fa --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/MultiStep.java @@ -0,0 +1,82 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2019 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.model; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * Holds required contextual information for a multi-step transform. + * + * @author Lucian Tuca + * created on 19/12/2018 + */ +public class MultiStep implements Serializable +{ + private String initialRequestId; + private String initialSourceMediaType; + private List transformsToBeDone = new ArrayList<>(); + + // regions [Accessors] + + public String getInitialSourceMediaType() + { + return initialSourceMediaType; + } + + public void setInitialSourceMediaType(String initialSourceMediaType) + { + this.initialSourceMediaType = initialSourceMediaType; + } + + public String getInitialRequestId() + { + return initialRequestId; + } + + public void setInitialRequestId(String initialRequestId) + { + this.initialRequestId = initialRequestId; + } + + public List getTransformsToBeDone() + { + return transformsToBeDone; + } + + public void setTransformsToBeDone(List transformsToBeDone) + { + this.transformsToBeDone = transformsToBeDone; + } + + //endregion + + @Override public String toString() + { + return "MultiStep{" + + "initialRequestId='" + initialRequestId + '\'' + + ", initialSourceMediaType='" + initialSourceMediaType + '\'' + + ", transformsToBeDone=" + transformsToBeDone + + '}'; + } +} diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/TransformReply.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/TransformReply.java new file mode 100644 index 00000000..b877fe94 --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/TransformReply.java @@ -0,0 +1,216 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2019 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.model; + +import java.io.Serializable; +import java.util.Objects; + +public class TransformReply implements Serializable +{ + private String requestId; + private int status; + private String errorDetails; + private String sourceReference; + private String targetReference; + private String clientData; + private int schema; + private InternalContext internalContext; + + //region [Accessors] + public String getRequestId() + { + return requestId; + } + + public void setRequestId(String requestId) + { + this.requestId = requestId; + } + + public int getStatus() + { + return status; + } + + public void setStatus(int status) + { + this.status = status; + } + + public String getErrorDetails() + { + return errorDetails; + } + + public void setErrorDetails(String errorDetails) + { + this.errorDetails = errorDetails; + } + + public String getSourceReference() + { + return sourceReference; + } + + public void setSourceReference(String sourceReference) + { + this.sourceReference = sourceReference; + } + + public String getTargetReference() + { + return targetReference; + } + + public void setTargetReference(String targetReference) + { + this.targetReference = targetReference; + } + + public String getClientData() + { + return clientData; + } + + public void setClientData(String clientData) + { + this.clientData = clientData; + } + + public int getSchema() + { + return schema; + } + + public void setSchema(int schema) + { + this.schema = schema; + } + + public InternalContext getInternalContext() + { + return internalContext; + } + + public void setInternalContext(InternalContext internalContext) + { + this.internalContext = internalContext; + } + + //endregion + + @Override + public boolean equals(Object o) + { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + TransformReply that = (TransformReply) o; + return Objects.equals(requestId, that.requestId); + } + + @Override + public int hashCode() + { + return Objects.hash(requestId); + } + + @Override public String toString() + { + return "TransformReply{" + + "requestId='" + requestId + '\'' + + ", status=" + status + + ", errorDetails='" + errorDetails + '\'' + + ", sourceReference='" + sourceReference + '\'' + + ", targetReference='" + targetReference + '\'' + + ", clientData='" + clientData + '\'' + + ", schema=" + schema + + ", internalContext=" + internalContext + + '}'; + } + + public static Builder builder() + { + return new Builder(); + } + + public static class Builder + { + private final TransformReply reply = new TransformReply(); + + private Builder() {} + + public Builder withRequestId(final String requestId) + { + reply.requestId = requestId; + return this; + } + + public Builder withStatus(final int status) + { + reply.status = status; + return this; + } + + public Builder withErrorDetails(final String errorDetails) + { + reply.errorDetails = errorDetails; + return this; + } + + public Builder withSourceReference(final String sourceReference) + { + reply.sourceReference = sourceReference; + return this; + } + + public Builder withTargetReference(final String targetReference) + { + reply.targetReference = targetReference; + return this; + } + + public Builder withClientData(final String clientData) + { + reply.clientData = clientData; + return this; + } + + public Builder withSchema(final int schema) + { + reply.schema = schema; + return this; + } + + public Builder withInternalContext(final InternalContext internalContext) + { + reply.internalContext = internalContext; + return this; + } + + public TransformReply build() + { + return reply; + } + } +} diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/TransformRequest.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/TransformRequest.java new file mode 100644 index 00000000..90784b4b --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/TransformRequest.java @@ -0,0 +1,273 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2019 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.model; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public class TransformRequest implements Serializable +{ + private String requestId; + private String sourceReference; + private String sourceMediaType; + private Long sourceSize; + private String sourceExtension; + private String targetMediaType; + private String targetExtension; + private String clientData; + private int schema; + private Map transformRequestOptions = new HashMap<>(); + private InternalContext internalContext; + + // regions [Accessors] + public String getRequestId() + { + return requestId; + } + + public void setRequestId(String requestId) + { + this.requestId = requestId; + } + + public String getSourceReference() + { + return sourceReference; + } + + public void setSourceReference(String sourceReference) + { + this.sourceReference = sourceReference; + } + + public String getSourceMediaType() + { + return sourceMediaType; + } + + public void setSourceMediaType(String sourceMediaType) + { + this.sourceMediaType = sourceMediaType; + } + + public Long getSourceSize() + { + return sourceSize; + } + + public void setSourceSize(Long sourceSize) + { + this.sourceSize = sourceSize; + } + + public String getSourceExtension() + { + return sourceExtension; + } + + public void setSourceExtension(String sourceExtension) + { + this.sourceExtension = sourceExtension; + } + + public String getTargetMediaType() + { + return targetMediaType; + } + + public void setTargetMediaType(String targetMediaType) + { + this.targetMediaType = targetMediaType; + } + + public String getTargetExtension() + { + return targetExtension; + } + + public void setTargetExtension(String targetExtension) + { + this.targetExtension = targetExtension; + } + + public String getClientData() + { + return clientData; + } + + public void setClientData(String clientData) + { + this.clientData = clientData; + } + + public int getSchema() + { + return schema; + } + + public void setSchema(int schema) + { + this.schema = schema; + } + + public Map getTransformRequestOptions() + { + return transformRequestOptions; + } + + public void setTransformRequestOptions(Map transformRequestOptions) + { + this.transformRequestOptions = transformRequestOptions; + } + + public InternalContext getInternalContext() + { + return internalContext; + } + + public void setInternalContext(InternalContext internalContext) + { + this.internalContext = internalContext; + } + + //endregion + + @Override + public boolean equals(Object o) + { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + TransformRequest that = (TransformRequest) o; + return Objects.equals(requestId, that.requestId); + } + + @Override + public int hashCode() + { + return Objects.hash(requestId); + } + + @Override public String toString() + { + return "TransformRequest{" + + "requestId='" + requestId + '\'' + + ", sourceReference='" + sourceReference + '\'' + + ", sourceMediaType='" + sourceMediaType + '\'' + + ", sourceSize=" + sourceSize + + ", sourceExtension='" + sourceExtension + '\'' + + ", targetMediaType='" + targetMediaType + '\'' + + ", targetExtension='" + targetExtension + '\'' + + ", clientData='" + clientData + '\'' + + ", schema=" + schema + + ", transformRequestOptions=" + transformRequestOptions + + ", internalContext=" + internalContext + + '}'; + } + + public static Builder builder() + { + return new Builder(); + } + + public static class Builder + { + private final TransformRequest request = new TransformRequest(); + + private Builder() {} + + public Builder withRequestId(final String requestId) + { + request.requestId = requestId; + return this; + } + + public Builder withInternalContext(final InternalContext internalContext) + { + request.internalContext = internalContext; + return this; + } + + public Builder withSourceReference(final String sourceReference) + { + request.sourceReference = sourceReference; + return this; + } + + public Builder withSourceMediaType(final String sourceMediaType) + { + request.sourceMediaType = sourceMediaType; + return this; + } + + public Builder withSourceSize(final Long sourceSize) + { + request.sourceSize = sourceSize; + return this; + } + + public Builder withSourceExtension(final String sourceExtension) + { + request.sourceExtension = sourceExtension; + return this; + } + + public Builder withTargetMediaType(final String targetMediaType) + { + request.targetMediaType = targetMediaType; + return this; + } + + public Builder withTargetExtension(final String targetExtension) + { + request.targetExtension = targetExtension; + return this; + } + + public Builder withClientData(final String clientData) + { + request.clientData = clientData; + return this; + } + + public Builder withTransformRequestOptions( + final Map transformRequestOptions) + { + request.transformRequestOptions = transformRequestOptions; + return this; + } + + public Builder withSchema(final int schema) + { + request.schema = schema; + return this; + } + + public TransformRequest build() + { + return request; + } + } +} diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/TransformRequestValidator.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/TransformRequestValidator.java new file mode 100644 index 00000000..fe9b3a13 --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/TransformRequestValidator.java @@ -0,0 +1,93 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.model; + +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; + +/** + * TransformRequestValidator + *

+ * Transform request validator + */ +public class TransformRequestValidator implements Validator +{ + @Override + public boolean supports(Class aClass) + { + return aClass.isAssignableFrom(TransformRequest.class); + } + + @Override + public void validate(Object o, Errors errors) + { + final TransformRequest request = (TransformRequest) o; + + if (request == null) + { + errors.reject(null, "request cannot be null"); + } + else + { + String requestId = request.getRequestId(); + if (requestId == null || requestId.isEmpty()) + { + errors.rejectValue("requestId", null, "requestId cannot be null or empty"); + } + Long sourceSize = request.getSourceSize(); + if (sourceSize == null || sourceSize <= 0) + { + errors.rejectValue("sourceSize", null, + "sourceSize cannot be null or have its value smaller than 0"); + } + String sourceMediaType = request.getSourceMediaType(); + if (sourceMediaType == null || sourceMediaType.isEmpty()) + { + errors.rejectValue("sourceMediaType", null, + "sourceMediaType cannot be null or empty"); + } + String targetMediaType = request.getTargetMediaType(); + if (targetMediaType == null || targetMediaType.isEmpty()) + { + errors.rejectValue("targetMediaType", null, + "targetMediaType cannot be null or empty"); + } + String targetExtension = request.getTargetExtension(); + if (targetExtension == null || targetExtension.isEmpty()) + { + errors.rejectValue("targetExtension", null, + "targetExtension cannot be null or empty"); + } + String clientData = request.getClientData(); + if (clientData == null || clientData.isEmpty()) + { + errors.rejectValue("clientData", String.valueOf(request.getSchema()), + "clientData cannot be null or empty"); + } + if (request.getSchema() < 0) + { + errors.rejectValue("schema", String.valueOf(request.getSchema()), + "schema cannot be less than 0"); + } + } + } +} diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/AbstractTransformOption.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/AbstractTransformOption.java new file mode 100644 index 00000000..d1357815 --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/AbstractTransformOption.java @@ -0,0 +1,76 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2019 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.model.config; + +import java.util.Objects; + +/** + * Abstract implementation of TransformOption. + */ +public abstract class AbstractTransformOption implements TransformOption +{ + private boolean required; + + public AbstractTransformOption() + { + } + + public AbstractTransformOption(boolean required) + { + this.required = required; + } + + @Override + public boolean isRequired() + { + return required; + } + + @Override + public void setRequired(boolean required) + { + this.required = required; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AbstractTransformOption that = (AbstractTransformOption) o; + return required == that.required; + } + + @Override + public int hashCode() + { + return Objects.hash(required); + } + + @Override + public String toString() + { + return "AbstractTransformOption{" + + "required=" + required + + '}'; + } +} diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/AddSupported.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/AddSupported.java new file mode 100644 index 00000000..c51405cd --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/AddSupported.java @@ -0,0 +1,53 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2021 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.model.config; + +/** + * Holds information to add new {@link SupportedSourceAndTarget} objects to an existing {@link Transformer}.


+ * + *

+ *   "addSupported": [
+ *     {
+ *       "transformerName": "Archive",
+ *       "sourceMediaType": "application/zip",
+ *       "targetMediaType": "text/xml",
+ *       "priority": 60,
+ *       "maxSourceSizeBytes": 18874368
+ *     }
+ *   ]
+ * 
+ */ +public class AddSupported extends TransformerTypesSizeAndPriority +{ + public static Builder builder() + { + return new Builder(); + } + + public static class Builder extends TransformerTypesSizeAndPriority.Builder + { + private Builder() + { + super(new AddSupported()); + } + } +} diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/CoreFunction.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/CoreFunction.java new file mode 100644 index 00000000..51f89b47 --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/CoreFunction.java @@ -0,0 +1,81 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.model.config; + +import org.apache.maven.artifact.versioning.ComparableVersion; + +import static org.alfresco.transform.client.model.config.CoreFunction.Constants.NO_UPPER_VERSION; +import static org.alfresco.transform.client.model.config.CoreFunction.Constants.NO_VERSION; + +/** + * Provides a mapping between a transform {@code coreVersion} and functionality (such as the use of Direct Access URLs) + * supported in that version of the {@code alfresco-transform-base}, so that clients know if they may use it. + */ +public enum CoreFunction +{ + /** May provide a Direct Access URL rather than upload a file **/ + DIRECT_ACCESS_URL("2.5.7", null), + + /** May request a transform via ActiveMQ **/ + // Original version was HTTP only. However none of these are still operational + ACTIVE_MQ("1", null), + + /** Original way to talk to a T-Engine **/ + // The toValue really should be null rather than "9999" but gives us an upper test value + HTTP(null, "99999"); + + private final ComparableVersion fromVersion; + private final ComparableVersion toVersion; + + public boolean isSupported(String version) + { + ComparableVersion comparableVersion = newComparableVersion(version, Constants.NO_VERSION); + return comparableVersion.compareTo(fromVersion) >= 0 && comparableVersion.compareTo(toVersion) <= 0; + } + + CoreFunction(String fromVersion, String toVersion) + { + this.fromVersion = newComparableVersion(fromVersion, NO_VERSION); + this.toVersion = newComparableVersion(toVersion, NO_UPPER_VERSION); + } + + static ComparableVersion newComparableVersion(String version, ComparableVersion defaultValue) + { + if (version == null) + { + return defaultValue; + } + + int i = version.indexOf('-'); + version = i > 0 + ? version.substring(0, i) + : version; + + return new ComparableVersion(version); + } + + static class Constants + { + static final ComparableVersion NO_VERSION = new ComparableVersion(""); + static final ComparableVersion NO_UPPER_VERSION = new ComparableVersion(Integer.toString(Integer.MAX_VALUE)); + } +} diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/CoreVersionDecorator.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/CoreVersionDecorator.java new file mode 100644 index 00000000..10d1649b --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/CoreVersionDecorator.java @@ -0,0 +1,182 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.model.config; + +import org.apache.maven.artifact.versioning.ComparableVersion; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static java.util.function.Predicate.not; +import static org.alfresco.transform.client.model.config.CoreFunction.Constants.NO_VERSION; +import static org.alfresco.transform.client.model.config.CoreFunction.newComparableVersion; +import static org.alfresco.transform.client.util.RequestParamMap.DIRECT_ACCESS_URL; + +/** + *

Class sets or clears the {@code coreVersion} property of {@link Transformer}s in a {@link TransformConfig}

+ * + *

Since alfresco-transform-core 5.2.7, the config returned by T-Engines and T-Router via their + * {@code "/transform/config"} endpoint has been decorated with an {@code coreVersion} element, indicating what core + * functionality is provided by each transformer as a result of extending the {@code AbstractTransformerController} in + * the {@code alfresco-transform-base}. This is automatically added, so need not be specified by the T-Engine developer. + * It was originally added to indicate that it was possible to use Direct Access URLs (DAU).

+ * + *

This class provides methods to sets or clear the field with the version number of the + * {@code alfresco-transform-base}. No value indicates 5.2.6 or earlier.

+ * + *

To allow older and newer version of the Repository, T-Router and T-Engines to work together, this field is only + * returned if requested by a client that also knows about the field. An optional {@code "configVersion"} parameter + * has been added to the endpoint. The config for T-Engines need only add the field to single-step-transforms. When + * configs are combined it is then possible to add this field to pipeline and failover transforms by using the lowest + * core value of any step transform.

+ * + *

If the field is not requested in the T-Router or the all-in-one transformer endpoint, it may need to be stripped + * from the {@link TransformConfig} as some of the T-Engines may have supplied it.

+ * + * @see CoreFunction + */ +public class CoreVersionDecorator +{ + public static final int CONFIG_VERSION_INCLUDES_CORE_VERSION = 2; + + private static final Set DIRECT_ACCESS_URL_TRANSFORM_OPTIONS = + Set.of(new TransformOptionValue(false, DIRECT_ACCESS_URL)); + + /** + * Returns a {@link TransformConfig} that includes or excludes the {@code coreVersion} field. + */ + public static TransformConfig setOrClearCoreVersion(TransformConfig transformConfig, int configVersion) + { + boolean includeCoreVersion = configVersion >= 2; + + Map> transformOptions = new HashMap<>(transformConfig.getTransformOptions()); + return TransformConfig.builder() + // We may need to create new Transformers as we must not change the original. + .withTransformers(transformConfig.getTransformers().stream() + .map(transformer -> { + if (( includeCoreVersion && transformer.getCoreVersion() == null) || + (!includeCoreVersion && transformer.getCoreVersion() != null)) + { + transformer = Transformer.builder() + .withCoreVersion(includeCoreVersion ? transformer.getCoreVersion() : null) + .withTransformOptions(setOrClearCoreTransformOptions( + includeCoreVersion ? transformer.getCoreVersion() : null, + transformOptions, transformer.getTransformOptions())) + // Original values + .withTransformerName(transformer.getTransformerName()) + .withTransformerPipeline(transformer.getTransformerPipeline()) + .withTransformerFailover(transformer.getTransformerFailover()) + .withSupportedSourceAndTargetList(transformer.getSupportedSourceAndTargetList()) + .build(); + } + return transformer; + }) + .collect(Collectors.toList())) + .withTransformOptions(transformOptions) + // Original values + .withRemoveTransformers(transformConfig.getRemoveTransformers()) + .withAddSupported(transformConfig.getAddSupported()) + .withRemoveSupported(transformConfig.getRemoveSupported()) + .withOverrideSupported(transformConfig.getOverrideSupported()) + .withSupportedDefaults(transformConfig.getSupportedDefaults()) + .build(); + } + + public static void setCoreVersionOnSingleStepTransformers(TransformConfig transformConfig, String coreVersion) + { + Map> transformOptions = transformConfig.getTransformOptions(); + List transformers = transformConfig.getTransformers(); + transformers.stream() + .filter(CoreVersionDecorator::isSingleStep) + .forEach(transformer -> { + transformer.setCoreVersion(coreVersion); + transformer.setTransformOptions(setOrClearCoreTransformOptions(coreVersion, + transformOptions, transformer.getTransformOptions())); + }); + } + + /** + * The list of {@code transformers} must not contain forward references + */ + public static void setCoreVersionOnMultiStepTransformers(Map> transformOptions, + List transformers) + { + Map transformersByName = transformers.stream() + .collect(Collectors.toMap(Transformer::getTransformerName, Function.identity())); + + transformers.stream() + .filter(not(CoreVersionDecorator::isSingleStep)) + .forEach(transformer -> { + + // Create a list of step transformers + List namesOfStepTransformers = transformer.getTransformerFailover().isEmpty() + ? transformer.getTransformerPipeline().stream() + .map(TransformStep::getTransformerName) + .collect(Collectors.toList()) + : transformer.getTransformerFailover(); + + // Set the coreVersion to the lowest step transformer value + ComparableVersion minCoreVersion = namesOfStepTransformers.stream() + .map(transformerName -> transformersByName.get(transformerName).getCoreVersion()) + .map(coreVersion -> newComparableVersion(coreVersion, NO_VERSION)) + .min(ComparableVersion::compareTo).orElse(NO_VERSION); + String coreVersion = NO_VERSION.equals(minCoreVersion) ? null : minCoreVersion.toString(); + transformer.setCoreVersion(coreVersion); + transformer.setTransformOptions(setOrClearCoreTransformOptions(transformer.getCoreVersion(), + transformOptions, transformer.getTransformOptions())); + }); + } + + private static Set setOrClearCoreTransformOptions(String coreVersion, Map> transformOptions, Set transformerTransformOptions) + { + // If we have more options being added in future, consider adding an interface that will be implemented by + // different implementations for each coreVersion and then iterate over them here. + transformerTransformOptions = new HashSet<>(transformerTransformOptions); + if (CoreFunction.DIRECT_ACCESS_URL.isSupported(coreVersion)) + { + // Added to the Transform config if any Transformers support it. + transformOptions.put(DIRECT_ACCESS_URL, DIRECT_ACCESS_URL_TRANSFORM_OPTIONS); + + // Add DIRECT_ACCESS_URL to a copy of this Transformer's transform options. + transformerTransformOptions.add(DIRECT_ACCESS_URL); + } + else + { + transformOptions.remove(DIRECT_ACCESS_URL); + transformerTransformOptions.remove(DIRECT_ACCESS_URL); + } + + return transformerTransformOptions; + } + + private static boolean isSingleStep(Transformer transformer) + { + return (transformer.getTransformerFailover() == null || transformer.getTransformerFailover().isEmpty()) && + (transformer.getTransformerPipeline() == null || transformer.getTransformerPipeline().isEmpty()); + } +} diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/OverrideSupported.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/OverrideSupported.java new file mode 100644 index 00000000..6a482152 --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/OverrideSupported.java @@ -0,0 +1,54 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2021 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.model.config; + +/** + * Holds information to overriding existing {@link SupportedSourceAndTarget} objects with new {@code priority} and + * {@code maxSourceSizeBytes} values.


+ * + *

+ *   "overrideSupported" : [
+ *     {
+ *       "transformerName": "Archive",             // override and existing entry
+ *       "sourceMediaType": "application/zip",
+ *       "targetMediaType": "text/html",
+ *       "priority": 60,
+ *       "maxSourceSizeBytes": 18874368
+ *     }
+ *   ]
+ * 
+ */ +public class OverrideSupported extends TransformerTypesSizeAndPriority +{ + public static Builder builder() + { + return new Builder(); + } + + public static class Builder extends TransformerTypesSizeAndPriority.Builder + { + private Builder() + { + super(new OverrideSupported()); + } + } +} diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/RemoveSupported.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/RemoveSupported.java new file mode 100644 index 00000000..29912859 --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/RemoveSupported.java @@ -0,0 +1,56 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2021 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.model.config; + +/** + * Holds information about existing {@link SupportedSourceAndTarget} objects that should be removed.


+ * + *

+ *   "removeSupported" : [
+ *     {
+ *       "transformerName": "Archive",
+ *       "sourceMediaType": "application/zip",
+ *       "targetMediaType": "text/xml"
+ *     }
+ *   ]
+ * 
+ */ +public class RemoveSupported extends TransformerAndTypes +{ + @Override + public String toString() + { + return "{"+super.toString()+"}"; + } + public static Builder builder() + { + return new Builder(); + } + + public static class Builder extends TransformerAndTypes.Builder + { + private Builder() + { + super(new RemoveSupported()); + } + } +} diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/SupportedDefaults.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/SupportedDefaults.java new file mode 100644 index 00000000..3a7b6542 --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/SupportedDefaults.java @@ -0,0 +1,182 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2021 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.model.config; + +import java.util.Objects; +import java.util.StringJoiner; + +/** + * Holds information to provide default {@code priority} and / or {@code maxSourceSizeBytes} defaults. In precedence + * order from most specific to most general these are defined by combinations of {@code transformerName} and + * {@code sourceMediaType}:


+ *

    + *
  • transformer and source media type default {@code transformerName} + {@code sourceMediaType}
  • + *
  • transformer default {@code transformerName}
  • + *
  • source media type default {@code sourceMediaType}
  • + *
  • system wide default none
  • + *

+ * + * Both {@code maxSourceSizeBytes} and {@code priority} may be specified in a {@code "supportedDefaults"} element, but + * if only one is specified it is only that value that is being defaulted at the level specified by the combination of + * {@code transformerName} and {@code sourceMediaType}.


+ * + *

+ *   "supportedDefaults" : [
+ *     {
+ *       "transformerName": "Office",              // default for a source type within a transformer
+ *       "sourceMediaType": "application/zip",
+ *       "maxSourceSizeBytes": 18874368
+ *     },
+ *     {
+ *       "sourceMediaType": "application/msword",  // defaults for a source type
+ *       "maxSourceSizeBytes": 4194304,
+ *       "priority": 45
+ *     },
+ *     {
+ *       "priority": 60                            // system default
+ *     }
+ *     {
+ *       "maxSourceSizeBytes": -1                  // system default
+ *     }
+ *   ]
+ * 
+ */ +public class SupportedDefaults +{ + String transformerName; + String sourceMediaType; + Long maxSourceSizeBytes = null; + Integer priority = null; + + public String getTransformerName() + { + return transformerName; + } + + public void setTransformerName(String transformerName) + { + this.transformerName = transformerName; + } + + public String getSourceMediaType() + { + return sourceMediaType; + } + + public void setSourceMediaType(String sourceMediaType) + { + this.sourceMediaType = sourceMediaType; + } + + public Long getMaxSourceSizeBytes() + { + return maxSourceSizeBytes; + } + + public void setMaxSourceSizeBytes(long maxSourceSizeBytes) + { + this.maxSourceSizeBytes = maxSourceSizeBytes; + } + + public Integer getPriority() + { + return priority; + } + + public void setPriority(int priority) + { + this.priority = priority; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SupportedDefaults that = (SupportedDefaults)o; + return Objects.equals(transformerName, that.transformerName) && + Objects.equals(sourceMediaType, that.sourceMediaType) && + Objects.equals(maxSourceSizeBytes, that.maxSourceSizeBytes) && + Objects.equals(priority, that.priority); + } + + @Override + public int hashCode() + { + return Objects.hash(transformerName, sourceMediaType, maxSourceSizeBytes, priority); + } + + @Override + public String toString() + { + StringJoiner sj = new StringJoiner(", "); + if (transformerName != null) sj.add("\"transformerName\": \""+transformerName+'"'); + if (sourceMediaType != null) sj.add("\"sourceMediaType\": \""+sourceMediaType+'"'); + if (maxSourceSizeBytes != null) sj.add("\"maxSourceSizeBytes\": \""+maxSourceSizeBytes+'"'); + if (priority != null) sj.add("\"priority\": \""+priority+'"'); + return "{" + sj.toString() + "}"; + } + + public static Builder builder() + { + return new Builder(); + } + + public static class Builder + { + final SupportedDefaults supportedDefaults; + + protected Builder() + { + this.supportedDefaults = new SupportedDefaults(); + } + + public SupportedDefaults build() + { + return supportedDefaults; + } + + public Builder withTransformerName(final String transformerName) + { + supportedDefaults.transformerName = transformerName; + return this; + } + + public Builder withSourceMediaType(final String sourceMediaType) + { + supportedDefaults.sourceMediaType = sourceMediaType; + return this; + } + + public Builder withMaxSourceSizeBytes(final Long maxSourceSizeBytes) + { + supportedDefaults.maxSourceSizeBytes = maxSourceSizeBytes; + return this; + } + + public Builder withPriority(final Integer priority) + { + supportedDefaults.priority = priority; + return this; + } + } +} diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/SupportedSourceAndTarget.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/SupportedSourceAndTarget.java new file mode 100644 index 00000000..4a36d67b --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/SupportedSourceAndTarget.java @@ -0,0 +1,112 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2021 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.model.config; + +import java.util.Objects; +import java.util.StringJoiner; + +/** + * Represents a single source and target combination supported by a transformer. Each combination has an optional + * maximum size and priority. + */ +public class SupportedSourceAndTarget extends Types +{ + Long maxSourceSizeBytes = null; + Integer priority = null; + + public SupportedSourceAndTarget() + { + } + + public Long getMaxSourceSizeBytes() + { + return maxSourceSizeBytes; + } + + public void setMaxSourceSizeBytes(Long maxSourceSizeBytes) + { + this.maxSourceSizeBytes = maxSourceSizeBytes; + } + + public Integer getPriority() + { + return priority; + } + + public void setPriority(Integer priority) + { + this.priority = priority; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + SupportedSourceAndTarget that = (SupportedSourceAndTarget)o; + return Objects.equals(maxSourceSizeBytes, that.maxSourceSizeBytes) && + Objects.equals(priority, that.priority); + } + + @Override + public int hashCode() + { + return Objects.hash(super.hashCode(), maxSourceSizeBytes, priority); + } + + @Override + public String toString() + { + StringJoiner sj = new StringJoiner(", "); + String superToString = super.toString(); + if (superToString != null) sj.add(superToString); + if (maxSourceSizeBytes != null) sj.add("\"maxSourceSizeBytes\": \""+maxSourceSizeBytes+'"'); + if (priority != null) sj.add("\"priority\": \""+priority+'"'); + return "{" + sj.toString() + "}"; + } + + public static Builder builder() + { + return new Builder(); + } + + public static class Builder extends Types.Builder + { + private Builder() + { + super(new SupportedSourceAndTarget()); + } + + public Builder withMaxSourceSizeBytes(final Long maxSourceSizeBytes) + { + t.setMaxSourceSizeBytes(maxSourceSizeBytes); + return this; + } + + public Builder withPriority(final Integer priority) + { + t.setPriority(priority); + return this; + } + } +} diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/TransformConfig.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/TransformConfig.java new file mode 100644 index 00000000..3b4b70a1 --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/TransformConfig.java @@ -0,0 +1,186 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2021 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.model.config; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * Transform Configuration with multiple {@link Transformer}s and {@link TransformOption}s. + * It can be used for one or more Transformers. + */ +public class TransformConfig +{ + private Map> transformOptions = new HashMap<>(); + + private List transformers = new ArrayList<>(); + + private Set removeTransformers = new HashSet<>(); + + private Set addSupported = new HashSet<>(); + + private Set removeSupported = new HashSet<>(); + + private Set overrideSupported = new HashSet<>(); + + private Set supportedDefaults = new HashSet<>(); + + public Map> getTransformOptions() + { + return transformOptions; + } + + public void setTransformOptions(Map> transformOptions) + { + this.transformOptions = transformOptions; + } + + public List getTransformers() + { + return transformers; + } + + public Set getRemoveTransformers() + { + return removeTransformers; + } + + public Set getAddSupported() + { + return addSupported; + } + + public Set getRemoveSupported() + { + return removeSupported; + } + + public Set getOverrideSupported() + { + return overrideSupported; + } + + public Set getSupportedDefaults() + { + return supportedDefaults; + } + + public void setTransformers(List transformers) + { + this.transformers = transformers; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TransformConfig that = (TransformConfig) o; + return transformOptions.equals(that.transformOptions) && + transformers.equals(that.transformers) && + removeTransformers.equals(that.removeTransformers) && + addSupported.equals(that.addSupported) && + removeSupported.equals(that.removeSupported) && + overrideSupported.equals(that.overrideSupported) && + supportedDefaults.equals(that.supportedDefaults); + } + + @Override + public int hashCode() + { + return Objects.hash(transformOptions, transformers, removeTransformers, addSupported, removeSupported, + overrideSupported, supportedDefaults); + } + + @Override + public String toString() + { + return "TransformConfig{" + + "transformOptions=" + transformOptions + + ", transformers=" + transformers + + '}'; + } + + public static Builder builder() + { + return new Builder(); + } + + public static class Builder + { + private final TransformConfig transformConfig = new TransformConfig(); + + private Builder() {} + + public TransformConfig build() + { + return transformConfig; + } + + public Builder withTransformOptions(final Map> transformOptions) + { + transformConfig.transformOptions = transformOptions; + return this; + } + + public Builder withTransformers(final List transformers) + { + transformConfig.transformers = transformers; + return this; + } + + public Builder withRemoveTransformers(final Set removeTransformers) + { + transformConfig.removeTransformers = removeTransformers; + return this; + } + + public Builder withAddSupported(final Set addSupported) + { + transformConfig.addSupported = addSupported; + return this; + } + + public Builder withRemoveSupported(final Set removeSupported) + { + transformConfig.removeSupported = removeSupported; + return this; + } + + public Builder withOverrideSupported(final Set overrideSupported) + { + transformConfig.overrideSupported = overrideSupported; + return this; + } + + public Builder withSupportedDefaults(final Set supportedDefaults) + { + transformConfig.supportedDefaults = supportedDefaults; + return this; + } + } +} diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/TransformOption.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/TransformOption.java new file mode 100644 index 00000000..e457f93f --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/TransformOption.java @@ -0,0 +1,39 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2019 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.model.config; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +/** + * Represents an individual transformer option or group of options that are required or optional (default). + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.WRAPPER_OBJECT) +@JsonSubTypes({@JsonSubTypes.Type(value = TransformOptionValue.class, name = "value"), + @JsonSubTypes.Type(value = TransformOptionGroup.class, name = "group")}) +public interface TransformOption +{ + boolean isRequired(); + + void setRequired(boolean required); +} diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/TransformOptionGroup.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/TransformOptionGroup.java new file mode 100644 index 00000000..c3b31439 --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/TransformOptionGroup.java @@ -0,0 +1,82 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2019 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.model.config; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +/** + * Represents a group of one or more options. If the group is optional, child options that are marked as required are + * only required if any child in the group is supplied by the client. If the group is required, child options are + * optional or required based on their own setting alone. + * + * In a pipeline transformation, a group of options + */ +public class TransformOptionGroup extends AbstractTransformOption +{ + private Set transformOptions = new HashSet<>(); + + public TransformOptionGroup() + { + } + + public TransformOptionGroup(boolean required, Set transformOptions) + { + super(required); + this.transformOptions = transformOptions; + } + + public Set getTransformOptions() + { + return transformOptions; + } + + public void setTransformOptions(Set transformOptions) + { + this.transformOptions = transformOptions; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + TransformOptionGroup that = (TransformOptionGroup) o; + return Objects.equals(transformOptions, that.transformOptions); + } + + @Override + public int hashCode() + { + return Objects.hash(super.hashCode(), transformOptions); + } + + @Override + public String toString() + { + return "TransformOptionGroup{" + + "transformOptions=" + transformOptions + + '}'; + } +} \ No newline at end of file diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/TransformOptionValue.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/TransformOptionValue.java new file mode 100644 index 00000000..2e4841dd --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/TransformOptionValue.java @@ -0,0 +1,76 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2019 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.model.config; + +import java.util.Objects; + +/** + * Represents a single transformation option. + */ +public class TransformOptionValue extends AbstractTransformOption +{ + private String name; + + public TransformOptionValue() + { + } + + public TransformOptionValue(boolean required, String name) + { + super(required); + this.name = name; + } + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + TransformOptionValue that = (TransformOptionValue) o; + return Objects.equals(name, that.name); + } + + @Override + public int hashCode() + { + return Objects.hash(super.hashCode(), name); + } + + @Override + public String toString() + { + return "TransformOptionValue{" + + "name='" + name + '\'' + + '}'; + } +} diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/TransformStep.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/TransformStep.java new file mode 100644 index 00000000..49eb5839 --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/TransformStep.java @@ -0,0 +1,89 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2019 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.model.config; + +import java.util.Objects; + +/** + * Represents a single transform step in a transform pipeline. The last step in the pipeline does not specify the + * target type as that is based on the supported types and what has been requested. + */ +public class TransformStep +{ + private String transformerName; + private String targetMediaType; + + public TransformStep() + { + } + + public TransformStep(String transformerName, String targetMediaType) + { + this.transformerName = transformerName; + this.targetMediaType = targetMediaType; + } + + public String getTransformerName() + { + return transformerName; + } + + public void setTransformerName(String transformerName) + { + this.transformerName = transformerName; + } + + public String getTargetMediaType() + { + return targetMediaType; + } + + public void setTargetMediaType(String targetMediaType) + { + this.targetMediaType = targetMediaType; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TransformStep that = (TransformStep) o; + return Objects.equals(transformerName, that.transformerName) && + Objects.equals(targetMediaType, that.targetMediaType); + } + + @Override + public int hashCode() + { + return Objects.hash(transformerName, targetMediaType); + } + + @Override + public String toString() + { + return "TransformStep{" + + "transformerName='" + transformerName + '\'' + + ", targetMediaType='" + targetMediaType + '\'' + + '}'; + } +} diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/Transformer.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/Transformer.java new file mode 100644 index 00000000..b363034e --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/Transformer.java @@ -0,0 +1,244 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.model.config; + +import org.alfresco.transform.client.registry.TransformServiceRegistry; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/** + * Represents a set of transformations supported by the Transform Service or Local Transform Service Registry that + * share the same transform options. Each may be an actual transformer, a pipeline of multiple transformers or a list + * of failover transforms. It is possible that more than one transformer may able to perform a transformation from one + * mimetype to another. The actual selection of transformer is up to the Transform Service or Local Transform Service + * Registry to decide. Clients may use + * {@link TransformServiceRegistry#isSupported(String, long, String, java.util.Map, String)} to decide + * if they should send a request to the service. As a result clients have a simple generic view of transformations which + * allows new transformations to be added without the need to change client data structures other than to define new + * name value pairs. For this to work the Transform Service defines unique names for each option. + *
    + *
  • transformerName - is optional but if supplied should be unique. The client should infer nothing from the name + * as it is simply a label, but the Local Transform Service Registry will use the name in pipelines.
  • + *
  • transformOptions - a grouping of individual transformer transformOptions. The group may be optional and may + * contain nested transformOptions.
  • + *
+ * For local transforms, this structure is extended when defining a pipeline transform and failover transform. + *
    + *
  • transformerPipeline - an array of pairs of transformer name and target extension for each transformer in the + * pipeline. The last one should not have an extension as that is defined by the request and should be in the + * supported list.
  • + *
  • transformerFailover - an array of failover definitions used in case of a fail transformation to pass a document + * to a sequence of transforms until one succeeds.
  • + *
  • coreVersion - indicates the version of the T-Engine's base. See {@link CoreVersionDecorator} for more detail.
  • + *
+ */ +public class Transformer +{ + private String transformerName; + private String coreVersion; + private Set transformOptions = new HashSet<>(); + private Set supportedSourceAndTargetList = new HashSet<>(); + private List transformerPipeline = new ArrayList<>(); + private List transformerFailover = new ArrayList<>(); + + public Transformer() + { + } + + public Transformer(String transformerName, Set transformOptions, + Set supportedSourceAndTargetList) + { + this.transformerName = transformerName; + this.transformOptions = transformOptions; + this.supportedSourceAndTargetList = supportedSourceAndTargetList; + } + + public Transformer(String transformerName, Set transformOptions, + Set supportedSourceAndTargetList, + List transformerPipeline) + { + this(transformerName, transformOptions, supportedSourceAndTargetList); + this.transformerPipeline = transformerPipeline; + } + + public Transformer(String transformerName, Set transformOptions, + Set supportedSourceAndTargetList, + List transformerPipeline, List transformerFailover) + { + this(transformerName, transformOptions, supportedSourceAndTargetList, transformerPipeline); + this.transformerFailover = transformerFailover; + } + + public String getTransformerName() + { + return transformerName; + } + + public void setTransformerName(String transformerName) + { + this.transformerName = transformerName; + } + + public String getCoreVersion() + { + return coreVersion; + } + + public void setCoreVersion(String coreVersion) + { + this.coreVersion = coreVersion; + } + + public List getTransformerPipeline() + { + return transformerPipeline; + } + + public void setTransformerPipeline(List transformerPipeline) + { + this.transformerPipeline = transformerPipeline; + } + + public List getTransformerFailover() + { + return transformerFailover; + } + + public void setTransformerFailover(List transformerFailover) + { + this.transformerFailover = transformerFailover; + } + + public Set getTransformOptions() + { + return transformOptions; + } + + public void setTransformOptions(Set transformOptions) + { + this.transformOptions = transformOptions; + } + + public Set getSupportedSourceAndTargetList() + { + return supportedSourceAndTargetList; + } + + public void setSupportedSourceAndTargetList( + Set supportedSourceAndTargetList) + { + this.supportedSourceAndTargetList = supportedSourceAndTargetList; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Transformer that = (Transformer) o; + return Objects.equals(transformerName, that.transformerName) && + Objects.equals(coreVersion, that.coreVersion) && + Objects.equals(transformerPipeline, that.transformerPipeline) && + Objects.equals(transformerFailover, that.transformerFailover) && + Objects.equals(transformOptions, that.transformOptions) && + Objects.equals(supportedSourceAndTargetList, + that.supportedSourceAndTargetList); + } + + @Override + public int hashCode() + { + return Objects.hash(transformerName, coreVersion, transformerPipeline, transformerFailover, transformOptions, + supportedSourceAndTargetList); + } + + @Override + public String toString() + { + return "Transformer{" + + "transformerName='" + transformerName + '\'' + + ", coreVersion=" + coreVersion + + ", transformerPipeline=" + transformerPipeline + + ", transformerFailover=" + transformerFailover + + ", transformOptions=" + transformOptions + + ", supportedSourceAndTargetList=" + supportedSourceAndTargetList + + '}'; + } + + public static Builder builder() + { + return new Builder(); + } + + public static class Builder + { + private final Transformer transformer = new Transformer(); + + private Builder() {} + + public Transformer build() + { + return transformer; + } + + public Builder withTransformerName(final String transformerName) + { + transformer.transformerName = transformerName; + return this; + } + + public Builder withCoreVersion(final String coreVersion) + { + transformer.coreVersion = coreVersion; + return this; + } + + public Builder withTransformerPipeline(final List transformerPipeline) + { + transformer.transformerPipeline = transformerPipeline; + return this; + } + + public Builder withTransformerFailover(final List transformerFailover) + { + transformer.transformerFailover = transformerFailover; + return this; + } + + public Builder withTransformOptions(final Set transformOptions) + { + transformer.transformOptions = transformOptions; + return this; + } + + public Builder withSupportedSourceAndTargetList( + final Set supportedSourceAndTargetList) + { + transformer.supportedSourceAndTargetList = supportedSourceAndTargetList; + return this; + } + } +} diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/TransformerAndTypes.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/TransformerAndTypes.java new file mode 100644 index 00000000..a065113d --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/TransformerAndTypes.java @@ -0,0 +1,90 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2021 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.model.config; + +import java.util.Objects; +import java.util.StringJoiner; + +/** + * Base object with {@code transformerName}, {@code sourceMediaType}and {@code targetMediaType}. + * Used to identify supported transforms. + */ +public abstract class TransformerAndTypes extends Types +{ + String transformerName; + + protected TransformerAndTypes() {} + + public String getTransformerName() + { + return transformerName; + } + + public void setTransformerName(String transformerName) + { + this.transformerName = transformerName; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + TransformerAndTypes that = (TransformerAndTypes) o; + return Objects.equals(transformerName, that.transformerName); + } + + @Override + public int hashCode() + { + return Objects.hash(super.hashCode(), transformerName); + } + + @Override + public String toString() + { + StringJoiner sj = new StringJoiner(", "); + String superToString = super.toString(); + if (transformerName != null) sj.add("\"transformerName\": \""+transformerName+'"'); + if (superToString != null) sj.add(superToString); + return sj.toString(); + } + + public static class Builder + extends Types.Builder + { + private final T t; + + protected Builder(T t) + { + super(t); + this.t = t; + } + + public B withTransformerName(final String transformerName) + { + t.transformerName = transformerName; + return (B)this; + } + } +} diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/TransformerTypesSizeAndPriority.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/TransformerTypesSizeAndPriority.java new file mode 100644 index 00000000..5bcdcca2 --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/TransformerTypesSizeAndPriority.java @@ -0,0 +1,109 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2021 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.model.config; + +import java.util.Objects; +import java.util.StringJoiner; + +/** + * Base object with {@code transformerName}, {@code sourceMediaType}, {@code targetMediaType}, + * {@code maxSourceSizeBytes} and {@code priority}. + */ +public abstract class TransformerTypesSizeAndPriority extends TransformerAndTypes +{ + Long maxSourceSizeBytes = null; + Integer priority = null; + + protected TransformerTypesSizeAndPriority() {} + + public Long getMaxSourceSizeBytes() + { + return maxSourceSizeBytes; + } + + public void setMaxSourceSizeBytes(Long maxSourceSizeBytes) + { + this.maxSourceSizeBytes = maxSourceSizeBytes; + } + + public Integer getPriority() + { + return priority; + } + + public void setPriority(Integer priority) + { + this.priority = priority; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + TransformerTypesSizeAndPriority that = (TransformerTypesSizeAndPriority)o; + return getMaxSourceSizeBytes().equals(that.getMaxSourceSizeBytes()) && + getPriority() == that.getPriority(); + } + + @Override + public int hashCode() + { + return Objects.hash(super.hashCode(), transformerName); + } + + @Override + public String toString() + { + StringJoiner sj = new StringJoiner(", "); + String superToString = super.toString(); + if (superToString != null) sj.add(superToString); + if (maxSourceSizeBytes != null) sj.add("\"maxSourceSizeBytes\": \""+maxSourceSizeBytes+'"'); + if (priority != null) sj.add("\"priority\": \""+priority+'"'); + return "{"+sj.toString()+"}"; + } + + public static class Builder + extends TransformerAndTypes.Builder + { + private final T t; + + protected Builder(T t) + { + super(t); + this.t = t; + } + + public B withMaxSourceSizeBytes(final long maxSourceSizeBytes) + { + t.setMaxSourceSizeBytes(maxSourceSizeBytes); + return (B)this; + } + + public B withPriority(final int priority) + { + t.setPriority(priority); + return (B)this; + } + } +} diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/Types.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/Types.java new file mode 100644 index 00000000..06174258 --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/model/config/Types.java @@ -0,0 +1,109 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2021 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.model.config; + +import java.util.Objects; +import java.util.StringJoiner; + +/** + * Base object with {@code sourceMediaType} and {@code targetMediaType}. + * Used to identify supported transforms. + */ +public class Types +{ + String sourceMediaType; + String targetMediaType; + + protected Types() {} + + public String getSourceMediaType() + { + return sourceMediaType; + } + + public void setSourceMediaType(String sourceMediaType) + { + this.sourceMediaType = sourceMediaType; + } + + public String getTargetMediaType() + { + return targetMediaType; + } + + public void setTargetMediaType(String targetMediaType) + { + this.targetMediaType = targetMediaType; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Types that = (Types) o; + return Objects.equals(sourceMediaType, that.sourceMediaType) && + Objects.equals(targetMediaType, that.targetMediaType); + } + + @Override + public int hashCode() + { + return Objects.hash(sourceMediaType, targetMediaType); + } + + @Override + public String toString() + { + StringJoiner sj = new StringJoiner(", "); + if (sourceMediaType != null) sj.add("\"sourceMediaType\": \""+sourceMediaType+'"'); + if (targetMediaType != null) sj.add("\"targetMediaType\": \""+targetMediaType+'"'); + return sj.toString(); + } + + public static abstract class Builder + { + final T t; + + protected Builder(T t) + { + this.t = t; + } + + public T build() + { + return (T)t; + } + + public B withSourceMediaType(final String sourceMediaType) + { + t.sourceMediaType = sourceMediaType; + return (B)this; + } + + public B withTargetMediaType(final String targetMediaType) + { + t.targetMediaType = targetMediaType; + return (B)this; + } + } +} diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/client/registry/AbstractTransformRegistry.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/registry/AbstractTransformRegistry.java new file mode 100644 index 00000000..d4c26d94 --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/registry/AbstractTransformRegistry.java @@ -0,0 +1,144 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.registry; + +import static org.alfresco.transform.client.registry.TransformRegistryHelper.retrieveTransformListBySize; +import static org.alfresco.transform.client.registry.TransformRegistryHelper.lookupTransformOptions; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.transform.client.model.config.CoreFunction; +import org.alfresco.transform.client.model.config.TransformOption; +import org.alfresco.transform.client.model.config.Transformer; + +/** + * Used to work out if a transformation is supported. Sub classes should implement {@link #getData()} to return an + * instance of the {@link TransformCache} class. This allows sub classes to periodically replace the registry's data with newer + * values. They may also extend the Data class to include extra fields and methods. + */ +public abstract class AbstractTransformRegistry implements TransformServiceRegistry +{ + /** + * Logs an error message if there is an error in the configuration. + * + * @param msg to be logged. + */ + protected abstract void logError(String msg); + + /** + * Logs a warning message if there is a problem in the configuration. + * + * @param msg to be logged. + */ + protected void logWarn(String msg) + { + logError(msg); + } + + /** + * Returns the data held by the registry. Sub classes may extend the base Data and replace it at run time. + * + * @return the Data object that contains the registry's data. + */ + public abstract TransformCache getData(); + + /** + * Registers a single transformer. This is an internal method called by + * {@link CombinedTransformConfig#registerCombinedTransformers(AbstractTransformRegistry)}. + * + * @param transformer to be registered + * @param transformOptions all the transform options + * @param baseUrl where the config was be read from. Only needed when it is remote. Is null when local. + * Does not need to be a URL. May just be a name. + * @param readFrom debug message for log messages, indicating what type of config was read. + */ + protected void register(final Transformer transformer, + final Map> transformOptions, final String baseUrl, + final String readFrom) + { + getData().incrementTransformerCount(); + transformer + .getSupportedSourceAndTargetList() + .forEach(e -> getData().appendTransform(e.getSourceMediaType(), e.getTargetMediaType(), + new SupportedTransform( + transformer.getTransformerName(), + lookupTransformOptions(transformer.getTransformOptions(), transformOptions, + readFrom, this::logError), + e.getMaxSourceSizeBytes(), + e.getPriority()), + transformer.getTransformerName(), + transformer.getCoreVersion())); + } + + /** + * Works out the name of the transformer (might not map to an actual transformer) that will be used to transform + * content of a given source mimetype and size into a target mimetype given a list of actual transform option names + * and values (Strings) plus the data contained in the Transform objects registered with this class. + * + * @param sourceMimetype the mimetype of the source content + * @param sourceSizeInBytes the size in bytes of the source content. Ignored if negative. + * @param targetMimetype the mimetype of the target + * @param actualOptions the actual name value pairs available that could be passed to the Transform Service. + * @param renditionName (optional) name for the set of options and target mimetype. If supplied is used to cache + * results to avoid having to work out if a given transformation is supported a second time. + * The sourceMimetype and sourceSizeInBytes may still change. In the case of ACS this is the + * rendition name. + */ + @Override + public String findTransformerName(final String sourceMimetype, final long sourceSizeInBytes, + final String targetMimetype, final Map actualOptions, + final String renditionName) + { + return retrieveTransformListBySize(getData(), sourceMimetype, targetMimetype, actualOptions, + renditionName) + .stream() + .filter(t -> t.getMaxSourceSizeBytes() == -1 || + t.getMaxSourceSizeBytes() >= sourceSizeInBytes) + .findFirst() + .map(SupportedTransform::getName) + .orElse(null); + } + + @Override + public long findMaxSize(final String sourceMimetype, final String targetMimetype, + final Map actualOptions, final String renditionName) + { + final List supportedTransforms = retrieveTransformListBySize(getData(), + sourceMimetype, targetMimetype, actualOptions, renditionName); + return supportedTransforms.isEmpty() ? 0 : + supportedTransforms.get(supportedTransforms.size() - 1).getMaxSourceSizeBytes(); + } + + @Override + public boolean isSupported(CoreFunction function, String transformerName) + { + return function.isSupported(getData().getCoreVersion(transformerName)); + } + + // When testing, we need to be able to set the baseUrl when reading from a file. + public String getBaseUrlIfTesting(String name, String baseUrl) + { + return baseUrl; + } +} diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/client/registry/CombinedTransformConfig.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/registry/CombinedTransformConfig.java new file mode 100644 index 00000000..522a4d23 --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/registry/CombinedTransformConfig.java @@ -0,0 +1,830 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.registry; + +import org.alfresco.transform.client.model.config.AddSupported; +import org.alfresco.transform.client.model.config.SupportedDefaults; +import org.alfresco.transform.client.model.config.OverrideSupported; +import org.alfresco.transform.client.model.config.RemoveSupported; +import org.alfresco.transform.client.model.config.SupportedSourceAndTarget; +import org.alfresco.transform.client.model.config.TransformConfig; +import org.alfresco.transform.client.model.config.TransformOption; +import org.alfresco.transform.client.model.config.TransformStep; +import org.alfresco.transform.client.model.config.Transformer; +import org.alfresco.transform.client.model.config.TransformerAndTypes; +import org.alfresco.transform.client.model.config.Types; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.StringJoiner; +import java.util.stream.Collectors; + +import static java.util.stream.Collectors.toSet; +import static org.alfresco.transform.client.model.config.CoreVersionDecorator.setCoreVersionOnMultiStepTransformers; + +/** + * This class combines one or more T-Engine config and local files and registers them as if they were all in one file. + * Transform options are shared between all sources.


+ * + * The caller should make calls to {@link #addTransformConfig(TransformConfig, String, String, AbstractTransformRegistry)} + * followed by calls to {@link #combineTransformerConfig(AbstractTransformRegistry)} and then + * {@link #registerCombinedTransformers(AbstractTransformRegistry)}.


+ * + * The helper method {@link #combineAndRegister(TransformConfig, String, String, AbstractTransformRegistry)} may be used + * when there is only one config. + * + * @author adavis + */ +public class CombinedTransformConfig +{ + private static final String MIMETYPE_METADATA_EXTRACT = "alfresco-metadata-extract"; + private static final String MIMETYPE_METADATA_EMBED = "alfresco-metadata-embed"; + + private static final String INTERMEDIATE_STEPS_SHOULD_HAVE_A_TARGET_MIMETYPE = "intermediate steps should have a target mimetype"; + private static final String THE_FINAL_STEP_SHOULD_NOT_HAVE_A_TARGET_MIMETYPE = "the final step should not have a target mimetype"; + + private final Map> combinedTransformOptions = new HashMap<>(); + private List> combinedTransformers = new ArrayList<>(); + private final Defaults defaults = new Defaults(); + + public static void combineAndRegister(TransformConfig transformConfig, String readFrom, String baseUrl, + AbstractTransformRegistry registry) + { + CombinedTransformConfig combinedTransformConfig = new CombinedTransformConfig(); + combinedTransformConfig.addTransformConfig(transformConfig, readFrom, baseUrl, registry); + combinedTransformConfig.combineTransformerConfig(registry); + combinedTransformConfig.registerCombinedTransformers(registry); + } + + public void clear() + { + combinedTransformOptions.clear(); + combinedTransformers.clear(); + defaults.clear(); + } + + public void addTransformConfig(List> transformConfigList, AbstractTransformRegistry registry) + { + transformConfigList.forEach(tc -> addTransformConfig(tc.get(), tc.getReadFrom(), tc.getBaseUrl(), registry)); + } + + public void addTransformConfig(TransformConfig transformConfig, String readFrom, String baseUrl, + AbstractTransformRegistry registry) + { + removeTransformers(transformConfig.getRemoveTransformers(), readFrom, registry); + supportedDefaults(transformConfig.getSupportedDefaults(), readFrom, registry); + + removeSupported(transformConfig.getRemoveSupported(), readFrom, registry); + addSupported(transformConfig.getAddSupported(), readFrom, registry); + overrideSupported(transformConfig.getOverrideSupported(), readFrom, registry); + + // Add transform options and transformers from the new transformConfig + transformConfig.getTransformOptions().forEach(combinedTransformOptions::put); + transformConfig.getTransformers().forEach(t -> combinedTransformers.add(new Origin<>(t, baseUrl, readFrom))); + } + + private void removeTransformers(Set removeTransformersSet, String readFrom, AbstractTransformRegistry registry) + { + if (!removeTransformersSet.isEmpty()) + { + Set leftOver = new HashSet<>(removeTransformersSet); + combinedTransformers.removeIf(combinedTransformer -> + { + String transformerName = combinedTransformer.get().getTransformerName(); + if (removeTransformersSet.contains(transformerName)) + { + leftOver.remove(transformerName); + return true; + } + return false; + }); + + Set quotedLeftOver = leftOver.stream().map(transformerName -> "\""+transformerName+'"').collect(toSet()); + logWarn(quotedLeftOver, readFrom, registry, "removeTransformers"); + } + } + + private void supportedDefaults(Set supportedDefaults, String readFrom, AbstractTransformRegistry registry) + { + if (!supportedDefaults.isEmpty()) + { + Set leftOver = new HashSet<>(supportedDefaults); + supportedDefaults.stream() + .filter(supportedDefault -> supportedDefault.getMaxSourceSizeBytes() != null || + supportedDefault.getPriority() != null) + .forEach(supportedDefault -> + { + defaults.add(supportedDefault); + leftOver.remove(supportedDefault); + }); + + logWarn(leftOver, readFrom, registry, "supportedDefaults"); + } + } + + private void processSupported( + Set tSet, String readFrom, AbstractTransformRegistry registry, String elementName, + ProcessSingleT process) + { + if (!tSet.isEmpty()) + { + Set leftOver = new HashSet<>(tSet); + tSet.stream() + .filter(t -> t.getTransformerName() != null && + t.getSourceMediaType() != null && + t.getTargetMediaType() != null) + .forEach(t -> process.process(leftOver, t)); + + logWarn(leftOver, readFrom, registry, elementName); + } + } + + private void logWarn(Set leftOver, String readFrom, AbstractTransformRegistry registry, String elementName) + { + if (!leftOver.isEmpty()) + { + StringJoiner sj = new StringJoiner(", ", + "Unable to process \"" + elementName + "\": [", "]. Read from " + readFrom); + leftOver.forEach(removeSupported -> sj.add(removeSupported.toString())); + registry.logWarn(sj.toString()); + } + } + + private interface ProcessSingleT + { + void process(Set leftOver, T removeSupported); + } + + private void removeSupported(Set removeSupportedSet, String readFrom, AbstractTransformRegistry registry) + { + processSupported(removeSupportedSet, readFrom, registry, "removeSupported", + (leftOver, removeSupported) -> + combinedTransformers.stream() + .map(Origin::get) + .forEach(transformer -> + { + if (transformer.getTransformerName().equals(removeSupported.getTransformerName()) && + transformer.getSupportedSourceAndTargetList().removeIf(supported -> + supported.getSourceMediaType().equals(removeSupported.getSourceMediaType()) && + supported.getTargetMediaType().equals(removeSupported.getTargetMediaType()))) + { + leftOver.remove(removeSupported); + } + })); + } + + private void addSupported(Set addSupportedSet, String readFrom, AbstractTransformRegistry registry) + { + processSupported(addSupportedSet, readFrom, registry, "addSupported", + (leftOver, addSupported) -> + combinedTransformers.stream() + .map(Origin::get) + .filter(transformer -> transformer.getTransformerName().equals(addSupported.getTransformerName())) + .forEach(transformerWithName -> + { + Set supportedSourceAndTargetList = + transformerWithName.getSupportedSourceAndTargetList(); + SupportedSourceAndTarget existingSupported = getExistingSupported( + supportedSourceAndTargetList, + addSupported.getSourceMediaType(), addSupported.getTargetMediaType()); + if (existingSupported == null) + { + SupportedSourceAndTarget newSupportedSourceAndTarget = SupportedSourceAndTarget.builder() + .withSourceMediaType(addSupported.getSourceMediaType()) + .withTargetMediaType(addSupported.getTargetMediaType()) + .withMaxSourceSizeBytes(addSupported.getMaxSourceSizeBytes()) + .withPriority(addSupported.getPriority()) + .build(); + supportedSourceAndTargetList.add(newSupportedSourceAndTarget); + leftOver.remove(addSupported); + }})); + } + + private void overrideSupported(Set overrideSupportedSet, String readFrom, AbstractTransformRegistry registry) + { + processSupported(overrideSupportedSet, readFrom, registry, "overrideSupported", + (leftOver, overrideSupported) -> + combinedTransformers.stream(). + map(Origin::get). + filter(transformer -> transformer.getTransformerName().equals(overrideSupported.getTransformerName())). + forEach(transformerWithName -> + { + Set supportedSourceAndTargetList = + transformerWithName.getSupportedSourceAndTargetList(); + SupportedSourceAndTarget existingSupported = getExistingSupported( + supportedSourceAndTargetList, + overrideSupported.getSourceMediaType(), overrideSupported.getTargetMediaType()); + if (existingSupported != null) + { + supportedSourceAndTargetList.remove(existingSupported); + existingSupported.setMaxSourceSizeBytes(overrideSupported.getMaxSourceSizeBytes()); + existingSupported.setPriority(overrideSupported.getPriority()); + supportedSourceAndTargetList.add(existingSupported); + leftOver.remove(overrideSupported); + } + })); + } + + private SupportedSourceAndTarget getExistingSupported(Set supportedSourceAndTargetList, + String sourceMediaType, String targetMediaType) + { + return supportedSourceAndTargetList.stream().filter(supported -> + supported.getSourceMediaType().equals(sourceMediaType) && + supported.getTargetMediaType().equals(targetMediaType)) + .findFirst() + .orElse(null); + } + + public void combineTransformerConfig(AbstractTransformRegistry registry) + { + removeInvalidTransformers(registry); + sortTransformers(registry); + applyDefaults(); + addWildcardSupportedSourceAndTarget(registry); + setCoreVersionOnCombinedMultiStepTransformers(); + } + + public TransformConfig buildTransformConfig() + { + List transformers = new ArrayList<>(); + combinedTransformers.forEach(ct->transformers.add(ct.get())); + Set supportedDefaults = defaults.getSupportedDefaults(); + return TransformConfig + .builder() + .withTransformers(transformers) + .withTransformOptions(combinedTransformOptions) + .withSupportedDefaults(supportedDefaults) + .build(); + } + + public void registerCombinedTransformers(AbstractTransformRegistry registry) + { + combinedTransformers.forEach(ct -> + registry.register(ct.get(), combinedTransformOptions, ct.getBaseUrl(), ct.getReadFrom())); + } + + /** + * Discards transformers that are invalid (e.g. transformers that have both pipeline and failover sections). Calls + * {@link #removeInvalidTransformer(int, List, AbstractTransformRegistry, Origin, Transformer, String, String, + * boolean, boolean)} for each transform, so that individual invalid transforms or overridden + * transforms may be discarded. + * + * @param registry that will hold the transforms. + */ + private void removeInvalidTransformers(AbstractTransformRegistry registry) + { + for (int i=0; i transformAndItsOrigin = combinedTransformers.get(i); + Transformer transformer = transformAndItsOrigin.get(); + String readFrom = transformAndItsOrigin.getReadFrom(); + String name = transformer.getTransformerName(); + List pipeline = transformer.getTransformerPipeline(); + List failover = transformer.getTransformerFailover(); + boolean isPipeline = pipeline != null && !pipeline.isEmpty(); + boolean isFailover = failover != null && !failover.isEmpty(); + + if (isPipeline && isFailover) + { + throw new IllegalArgumentException("Transformer " + transformerName(name) + + " cannot have pipeline and failover sections. Read from " + readFrom); + } + + // Remove transforms as they may override each other or be invalid + int indexToRemove = removeInvalidTransformer(i, combinedTransformers, registry, transformAndItsOrigin, + transformer, name, readFrom, isPipeline, isFailover); + + // Remove an overridden transform + if (indexToRemove >= 0) + { + combinedTransformers.remove(indexToRemove); + // The current index i should be decremented so we don't skip one. + // Condition not really needed as IllegalArgumentException is thrown if the latest entry is removed. + if (i >= indexToRemove) + { + i--; + } + } + } + catch (IllegalStateException e) + { + String msg = e.getMessage(); + registry.logWarn(msg); + combinedTransformers.remove(i--); + } + catch (IllegalArgumentException e) + { + String msg = e.getMessage(); + registry.logError(msg); + combinedTransformers.remove(i--); + } + } + } + + /** + * Discards a transformer that is + * 1) invalid: + * a) has no name + * b) the pass through transformer name is specified in a T-Engine + * c) specifies transform options that don't exist, + * d) has the same name as another T-Engine transform (i.e. there should be no duplicate names from t-engines), + * e) the pass through transformer name is specified in a pipeline file + * f) a single step transform defined outside a t-engine without it being an override, + * g) a pipeline or failover transform is being overridden by a single step transform. Invalid because we + * don't know if a t-engine will be able to do it. + * 2) an earlier transform with the same name (it is being overridden). If the overridden transform is from a + * T-Engine and the overriding transform is not a pipeline or a failover, we also copy the {@code baseUrl} + * from the overridden transform so that the original T-Engine will still be called. + * + * @param i the current transform's index into combinedTransformers. + * @param combinedTransformers the full list of transformers in the order they were read. + * @param registry that wil hold the transforms. + * @param transformAndItsOrigin the current combinedTransformers element. + * @param transformer the current transformer. + * @param name the current transformer's name. + * @param readFrom where the current transformer was read from. + * @param isPipeline if the current transform is a pipeline. + * @param isFailover if the current transform is a failover. + * + * @return the index of a transform to be removed. {@code -1} is returned if there should not be a remove. + * @throws IllegalArgumentException if the current transform has a problem and should be removed. + * @throws IllegalStateException if the current transform is dependent on config from another transform which + * is currently unavailable. + */ + private int removeInvalidTransformer(int i, List> combinedTransformers, + AbstractTransformRegistry registry, + Origin transformAndItsOrigin, Transformer transformer, + String name, String readFrom, boolean isPipeline, boolean isFailover) + { + int indexToRemove = -1; + + if (name == null || "".equals(name.trim())) + { + throw new IllegalArgumentException("Transformer names may not be null. Read from " + readFrom); + } + + // Get the baseUrl - test code might change it + String baseUrl = transformAndItsOrigin.getBaseUrl(); + String testBaseUrl = registry.getBaseUrlIfTesting(name, baseUrl); + if (!nullSafeEquals(baseUrl, testBaseUrl)) + { + baseUrl = testBaseUrl; + transformAndItsOrigin = new Origin<>(transformer, baseUrl, readFrom); + combinedTransformers.set(i, transformAndItsOrigin); + } + boolean isTEngineTransform = baseUrl != null; + + boolean isPassThroughTransform = isPassThroughTransformName(name); + if (isPassThroughTransform && isTEngineTransform) + { + throw new IllegalArgumentException("T-Engines should not use " + transformerName(name) + + " as a transform name. Read from " + readFrom); + } + + for (String transformOptionsLabel : transformer.getTransformOptions()) + { + if (!combinedTransformOptions.containsKey(transformOptionsLabel)) + { + throw new IllegalStateException("Transformer " + transformerName(name) + + " references \"" + transformOptionsLabel + + "\" which do not exist. Read from " + readFrom); + } + } + + boolean isOneStepTransform = !isPipeline && !isFailover && !isPassThroughTransform; + + // Check to see if the name has been used before. + int j = lastIndexOf(name, combinedTransformers, i); + if (j >= 0) + { + if (isTEngineTransform) + { + // We currently don't allow different t-engines to override each others single step transforms, + // but we could if the order is defined in which they are read. Would need to check the baseUrl + // is different. + throw new IllegalArgumentException("Transformer " + transformerName(name) + + " must be a unique name. Read from " + readFrom); + } + + if (isPassThroughTransform) + { + throw new IllegalArgumentException("Pipeline files should not use " + transformerName(name) + + " as a transform name. Read from " + readFrom); + } + + if (isOneStepTransform) + { + Origin overriddenTransformAndItsOrigin = combinedTransformers.get(j); + Transformer overriddenTransformer = overriddenTransformAndItsOrigin.get(); + List overriddenPipeline = overriddenTransformer.getTransformerPipeline(); + List overriddenFailover = overriddenTransformer.getTransformerFailover(); + boolean isOverriddenPipeline = overriddenPipeline != null && !overriddenPipeline.isEmpty(); + boolean isOverriddenFailover = overriddenFailover != null && !overriddenFailover.isEmpty(); + if (isOverriddenPipeline || isOverriddenFailover) + { + throw new IllegalArgumentException("Single step transformers (such as " + transformerName(name) + + ") may not override a pipeline or failover transform as there is no T-Engine to perform" + + " work. Read from " + readFrom); + } + + // We need to set the baseUrl of the original transform in the one overriding, + // so we can talk to its T-Engine + String overriddenBaseUrl = overriddenTransformAndItsOrigin.getBaseUrl(); + Transformer overriddenTransformTransform = transformAndItsOrigin.get(); + Origin overridingTransform = + new Origin<>(overriddenTransformTransform, overriddenBaseUrl, readFrom); + combinedTransformers.set(i, overridingTransform); + } + indexToRemove = j; + } + else if (isOneStepTransform && baseUrl == null) + { + throw new IllegalArgumentException("Single step transformers (such as " + transformerName(name) + + ") must be defined in a T-Engine rather than in a pipeline file, unless they are overriding " + + "an existing single step definition. Read from " + readFrom); + } + + return indexToRemove; + } + + protected boolean isPassThroughTransformName(String name) + { + return false; // There is no pass through transformer in ATS but there is in the Repo. + } + + private static int lastIndexOf(String name, List> combinedTransformers, int toIndex) + { + // Lists are short (< 100) entries and this is not a frequent or time critical step, so walking the list + // should be okay. + for (int j = toIndex-1; j >=0; j--) + { + Origin transformAndItsOrigin = combinedTransformers.get(j); + Transformer transformer = transformAndItsOrigin.get(); + String transformerName = transformer.getTransformerName(); + if (name.equals(transformerName)) + { + return j; + } + } + return -1; + } + + protected static String transformerName(String name) + { + return name == null ? " without a name" : "\"" + name + "\""; + } + + // Copied from EqualsHelper + private static boolean nullSafeEquals(Object left, Object right) + { + return (left == right) || (left != null && left.equals(right)); + } + + /** + * Sort transformers so there are no forward references, if that is possible. + * Logs warning message for those that have missing step transformers and removes them. + * @param registry used to log messages + */ + private void sortTransformers(AbstractTransformRegistry registry) + { + List> transformers = new ArrayList<>(combinedTransformers.size()); + List> todo = new ArrayList<>(combinedTransformers.size()); + Set transformerNames = new HashSet<>(); + boolean added; + do + { + added = false; + for (Origin transformAndItsOrigin : combinedTransformers) + { + Transformer transformer = transformAndItsOrigin.get(); + String name = transformer.getTransformerName(); + Set referencedTransformerNames = getReferencedTransformerNames(transformer); + + boolean addEntry = true; + for (String referencedTransformerName : referencedTransformerNames) + { + if (!transformerNames.contains(referencedTransformerName)) + { + todo.add(transformAndItsOrigin); + addEntry = false; + break; + } + } + + if (addEntry) + { + transformers.add(transformAndItsOrigin); + added = true; + if (name != null) + { + transformerNames.add(name); + } + } + } + combinedTransformers.clear(); + combinedTransformers.addAll(todo); + todo.clear(); + } + while (added && !combinedTransformers.isEmpty()); + + transformers.addAll(todo); + + for (Origin transformAndItsOrigin : combinedTransformers) + { + Transformer transformer = transformAndItsOrigin.get(); + String name = transformer.getTransformerName(); + registry.logWarn("Transformer " + transformerName(name) + + " ignored as step transforms " + getUnknownReferencedTransformerNames(transformer, transformerNames) + + " do not exist. Read from " + transformAndItsOrigin.getReadFrom()); + } + + this.combinedTransformers = transformers; + } + + private Set getReferencedTransformerNames(Transformer transformer) + { + Set referencedTransformerNames = new HashSet<>(); + List pipeline = transformer.getTransformerPipeline(); + if (pipeline != null) + { + for (TransformStep step : pipeline) + { + String stepName = step.getTransformerName(); + referencedTransformerNames.add(stepName); + } + } + List failover = transformer.getTransformerFailover(); + if (failover != null) + { + referencedTransformerNames.addAll(failover); + } + return referencedTransformerNames; + } + + private String getUnknownReferencedTransformerNames(Transformer transformer, Set transformerNames) + { + StringJoiner sj = new StringJoiner(", ", "(", ")"); + Set referencedTransformerNames = getReferencedTransformerNames(transformer); + for (String referencedTransformerName : referencedTransformerNames) + { + if (!transformerNames.contains(referencedTransformerName)) + { + sj.add(transformerName(referencedTransformerName)); + } + } + return sj.toString(); + } + + /** + * Applies priority and size defaults. Must be called before {@link #addWildcardSupportedSourceAndTarget(AbstractTransformRegistry)} + * as it uses the priority value. + */ + private void applyDefaults() + { + combinedTransformers.stream() + .map(Origin::get) + .forEach(transformer -> + { + transformer.setSupportedSourceAndTargetList( + transformer.getSupportedSourceAndTargetList().stream().map(supportedSourceAndTarget -> + { + Integer priority = supportedSourceAndTarget.getPriority(); + Long maxSourceSizeBytes = supportedSourceAndTarget.getMaxSourceSizeBytes(); + if (defaults.valuesUnset(priority, maxSourceSizeBytes)) + { + String transformerName = transformer.getTransformerName(); + String sourceMediaType = supportedSourceAndTarget.getSourceMediaType(); + supportedSourceAndTarget.setPriority(defaults.getPriority(transformerName, sourceMediaType, priority)); + supportedSourceAndTarget.setMaxSourceSizeBytes(defaults.getMaxSourceSizeBytes(transformerName, sourceMediaType, maxSourceSizeBytes)); + } + return supportedSourceAndTarget; + }).collect(toSet())); + }); + + defaults.clear(); + } + + /** + * When no supported source and target mimetypes have been defined in a failover or pipeline transformer + * this method adds all possible values that make sense. + * + *

  • Failover - all the supported values from the step transformers
  • + *
  • Pipeline - builds up supported source and target values. The list of source types and max sizes will come + * from the initial step transformer that have a target mimetype that matches the first + * intermediate mimetype. We then step through all intermediate transformers checking the next + * intermediate type is supported. When we get to the last step transformer, it provides all the + * target mimetypes based on the previous intermediate mimetype. Any combinations supported by + * the first transformer are excluded.
  • + * + * @param registry used to log messages + */ + private void addWildcardSupportedSourceAndTarget(AbstractTransformRegistry registry) + { + Map transformers = new HashMap<>(); + combinedTransformers.forEach(ct -> transformers.put(ct.get().getTransformerName(), ct.get())); + + combinedTransformers.forEach(transformAndItsOrigin -> + { + Transformer transformer = transformAndItsOrigin.get(); + + // If there are no SupportedSourceAndTarget, then work out all the wildcard combinations. + if (transformer.getSupportedSourceAndTargetList().isEmpty()) + { + List pipeline = transformer.getTransformerPipeline(); + List failover = transformer.getTransformerFailover(); + boolean isPipeline = pipeline != null && !pipeline.isEmpty(); + boolean isFailover = failover != null && !failover.isEmpty(); + String errorReason = null; + if (isFailover) + { + Set supportedSourceAndTargets = failover.stream().flatMap( + name -> transformers.get(name).getSupportedSourceAndTargetList().stream()). + collect(toSet()); + + // The failover transform might not be picked if the priority is the same as the step transforms + // so reduce it here by 1. In future we might want to specify the priority in the json, but for now + // it should be okay. + supportedSourceAndTargets.forEach(s->s.setPriority(s.getPriority()-1)); + transformer.setSupportedSourceAndTargetList(supportedSourceAndTargets); + errorReason = "the step transforms don't support any"; // only used if there are none + } + else if (isPipeline) + { + String sourceMediaType = null; + Set sourceMediaTypesAndMaxSizes = null; + Set firstTransformOptions = null; + String firstTransformStepName = null; + int numberOfSteps = pipeline.size(); + for (int stepIndex=0; stepIndex stepTrg.equals(s.getTargetMediaType())). + collect(toSet()); + sourceMediaType = stepTrg; + firstTransformOptions = stepTransformer.getTransformOptions(); + firstTransformStepName = name; + if (sourceMediaTypesAndMaxSizes.isEmpty()) + { + errorReason = "the first step transformer " + transformerName(name) + + " does not support to \"" + stepTrg + "\""; + break; + } + } + else + { + final String src = sourceMediaType; + if (stepIndex+1 == numberOfSteps) // if final step + { + if (stepTrg != null) + { + errorReason = THE_FINAL_STEP_SHOULD_NOT_HAVE_A_TARGET_MIMETYPE; + break; + } + + // Create a cartesian product of sourceMediaType,MaxSourceSize and TargetMediaType where + // the source matches the last intermediate. + Set supportedSourceAndTargets = sourceMediaTypesAndMaxSizes.stream(). + flatMap(s -> stepTransformer.getSupportedSourceAndTargetList().stream(). + filter(st -> + { + String targetMimetype = st.getTargetMediaType(); + return st.getSourceMediaType().equals(src) && + !(MIMETYPE_METADATA_EXTRACT.equals(targetMimetype) || + MIMETYPE_METADATA_EMBED.equals(targetMimetype)); + }). + map(Types::getTargetMediaType). + map(trg -> SupportedSourceAndTarget.builder(). + withSourceMediaType(s.getSourceMediaType()). + withMaxSourceSizeBytes(s.getMaxSourceSizeBytes()). + withPriority(s.getPriority()). + withTargetMediaType(trg).build())). + collect(toSet()); + + if (supportedSourceAndTargets.isEmpty()) + { + errorReason = "the final step transformer " + transformerName(name) + + " does not support from \"" + src + "\""; + } + else + { + // Exclude duplicates with the first transformer, if it has the same options. + // There is no point doing more work. + Set transformOptions = transformer.getTransformOptions(); + if (sameOptions(transformOptions, firstTransformOptions)) + { + supportedSourceAndTargets.removeAll(sourceMediaTypesAndMaxSizes); + } + if (supportedSourceAndTargets.isEmpty()) + { + errorReason = "the first transformer " + transformerName(firstTransformStepName) + + " in the pipeline already supported all source and target mimetypes" + + " that would have been added as wildcards"; + } + } + + transformer.setSupportedSourceAndTargetList(supportedSourceAndTargets); + } + else // if intermediate step + { + if (stepTrg == null) + { + errorReason = INTERMEDIATE_STEPS_SHOULD_HAVE_A_TARGET_MIMETYPE; + break; + } + + // Check source to target is supported (it normally is) + if (stepTransformer.getSupportedSourceAndTargetList().stream(). + noneMatch(st -> st.getSourceMediaType().equals(src) && + st.getTargetMediaType().equals(stepTrg))) + { + errorReason = "the step transformer " + + transformerName(stepTransformer.getTransformerName()) + " does not support \"" + + src + "\" to \"" + stepTrg + "\""; + break; + } + + sourceMediaType = stepTrg; + } + } + } + } + if (transformer.getSupportedSourceAndTargetList().isEmpty() && (isFailover || isPipeline)) + { + registry.logError("No supported source and target mimetypes could be added to the" + + " transformer " + transformerName(transformer.getTransformerName()) + " as " + errorReason + + ". Read from " + transformAndItsOrigin.getReadFrom()); + } + } + }); + } + + private boolean sameOptions(Set transformOptionNames1, Set transformOptionNames2) + { + // They have the same names + if (transformOptionNames1.equals(transformOptionNames2)) + { + return true; + } + + // Check the actual options. + Set transformOptions1 = getTransformOptions(transformOptionNames1); + Set transformOptions2 = getTransformOptions(transformOptionNames2); + return transformOptions1.equals(transformOptions2); + } + + private Set getTransformOptions(Set transformOptionNames) + { + Set transformOptions = new HashSet<>(); + transformOptionNames.forEach(name->transformOptions.addAll(combinedTransformOptions.get(name))); + return transformOptions; + } + + private void setCoreVersionOnCombinedMultiStepTransformers() + { + setCoreVersionOnMultiStepTransformers(combinedTransformOptions, combinedTransformers.stream() + .map(Origin::get) + .collect(Collectors.toList())); + } + + protected int transformerCount() + { + return combinedTransformers.size(); + } +} diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/client/registry/Defaults.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/registry/Defaults.java new file mode 100644 index 00000000..48d38f99 --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/registry/Defaults.java @@ -0,0 +1,148 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.registry; + +import org.alfresco.transform.client.model.config.SupportedDefaults; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +import static java.util.stream.Collectors.toSet; + +/** + * Maintains a list of defaults of {@code maxSourceSizeBytes} and {@code priority} keyed on + * {@code transformerName} and {@code sourceMediaType} so that it can provide a lookup when we + * join everything together in {@link CombinedTransformConfig#combineTransformerConfig(AbstractTransformRegistry)}. + * + * @see SupportedDefaults + */ +class Defaults +{ + private static final Integer DEFAULT_PRIORITY = 50; + private static final Long DEFAULT_MAX_SOURCE_SIZE_BYTES = -1L; + + private static final TransformerAndSourceType SYSTEM_WIDE_KEY = new TransformerAndSourceType(null, null); + + private final Map priorityDefaults = new HashMap<>(); + private final Map maxSourceSizeBytesDefaults = new HashMap<>(); + + public void add(SupportedDefaults supportedDefault) + { + TransformerAndSourceType key = + new TransformerAndSourceType(supportedDefault.getTransformerName(), supportedDefault.getSourceMediaType()); + Long maxSourceSizeBytes = supportedDefault.getMaxSourceSizeBytes(); + if (maxSourceSizeBytes != null) + { + maxSourceSizeBytesDefaults.put(key, maxSourceSizeBytes); + } + Integer priority = supportedDefault.getPriority(); + if (priority != null) + { + priorityDefaults.put(key, priority); + } + } + + Defaults() + { + clear(); + } + + public boolean valuesUnset(Integer supportedSourceAndTargetPriority, Long supportedSourceAndTargetMaxSourceSizeBytes) + { + return supportedSourceAndTargetPriority == null || supportedSourceAndTargetMaxSourceSizeBytes == null; + } + + public int getPriority(String transformerName, String sourceMediaType, Integer supportedSourceAndTargetPriority) + { + return getDefault(transformerName, sourceMediaType, supportedSourceAndTargetPriority, priorityDefaults); + } + + public long getMaxSourceSizeBytes(String transformerName, String sourceMediaType, Long supportedSourceAndTargetMaxSourceSizeBytes) + { + return getDefault(transformerName, sourceMediaType, supportedSourceAndTargetMaxSourceSizeBytes, maxSourceSizeBytesDefaults); + } + + private T getDefault(String transformerName, String sourceMediaType, T supportedSourceAndTargetValue, + Map map) + { + if (supportedSourceAndTargetValue != null) + { + return supportedSourceAndTargetValue; + } + + // 0: transformer and source media type default + // 1: transformer default + // 2: source media type default + // 3: system wide default + TransformerAndSourceType key = new TransformerAndSourceType(transformerName, sourceMediaType); + for (int i=0; ; i++) + { + T value = map.get(key); + if (value != null) + { + return value; + } + + switch (i) + { + case 0: + case 2: key.setSourceMediaType(null); break; + case 1: key.setSourceMediaType(sourceMediaType); key.setTransformerName(null); break; + default: throw new IllegalStateException("Should have found an entry with a null, null lookup"); + } + } + } + + public Set getSupportedDefaults() + { + return Stream.concat(maxSourceSizeBytesDefaults.keySet().stream(), priorityDefaults.keySet().stream()) + .filter(key -> + { + // Discard the entry added by clear() + return !SYSTEM_WIDE_KEY.equals(key) || + !DEFAULT_MAX_SOURCE_SIZE_BYTES.equals(maxSourceSizeBytesDefaults.get(key)) || + !DEFAULT_PRIORITY.equals(priorityDefaults.get(key)); + }) + .map(key -> + { + Long maxSourceSizeBytes = maxSourceSizeBytesDefaults.get(key); + Integer priority = priorityDefaults.get(key); + return SupportedDefaults.builder() + .withTransformerName(key.getTransformerName()) + .withSourceMediaType(key.getSourceMediaType()) + .withPriority(priority) + .withMaxSourceSizeBytes(maxSourceSizeBytes) + .build(); + }).collect(toSet()); + } + + public void clear() + { + priorityDefaults.clear(); + maxSourceSizeBytesDefaults.clear(); + + priorityDefaults.put(SYSTEM_WIDE_KEY, DEFAULT_PRIORITY); + maxSourceSizeBytesDefaults.put(SYSTEM_WIDE_KEY, DEFAULT_MAX_SOURCE_SIZE_BYTES); + } +} diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/client/registry/Origin.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/registry/Origin.java new file mode 100644 index 00000000..d6cd769b --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/registry/Origin.java @@ -0,0 +1,82 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2021 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.registry; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +/** + * Wraps an object so that we know where it was read from. The equals() and hashcode() are that of the wrapped object + * so it is still possible do set operations. + */ +public class Origin +{ + final T t; + final String baseUrl; + final String readFrom; + + public Origin(T t, String baseUrl, String readFrom) + { + this.t = t; + this.baseUrl = baseUrl; + this.readFrom = readFrom; + } + + public T get() + { + return t; + } + public String getBaseUrl() + { + return baseUrl; + } + + public String getReadFrom() + { + return readFrom; + } + + public static Set setOf(Collection> originCollection) + { + Set tSet = new HashSet<>(originCollection.size()); + originCollection.forEach(element -> tSet.add(element.get())); + return tSet; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Origin origin = (Origin)o; + return t.equals(origin.t); + } + + @Override + public int hashCode() + { + return Objects.hash(t); + } +} + diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/client/registry/SupportedTransform.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/registry/SupportedTransform.java new file mode 100644 index 00000000..d2680b8c --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/registry/SupportedTransform.java @@ -0,0 +1,92 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2019 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.registry; + +import java.util.Objects; +import java.util.Set; + +import org.alfresco.transform.client.model.config.TransformOption; +import org.alfresco.transform.client.model.config.TransformOptionGroup; + +public class SupportedTransform +{ + private final TransformOptionGroup transformOptions; + private final long maxSourceSizeBytes; + private final String name; + private final int priority; + + SupportedTransform(String name, Set transformOptions, + long maxSourceSizeBytes, int priority) + { + // Logically the top level TransformOptionGroup is required, so that child options are optional or required + // based on their own setting. + this.transformOptions = new TransformOptionGroup(true, transformOptions); + this.maxSourceSizeBytes = maxSourceSizeBytes; + this.name = name; + this.priority = priority; + } + + public TransformOptionGroup getTransformOptions() + { + return transformOptions; + } + + public long getMaxSourceSizeBytes() + { + return maxSourceSizeBytes; + } + + public String getName() + { + return name; + } + + public int getPriority() + { + return priority; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SupportedTransform that = (SupportedTransform) o; + return maxSourceSizeBytes == that.maxSourceSizeBytes && + priority == that.priority && + Objects.equals(transformOptions, that.transformOptions) && + Objects.equals(name, that.name); + } + + @Override + public int hashCode() + { + return Objects.hash(transformOptions, maxSourceSizeBytes, name, priority); + } + + @Override + public String toString() + { + return name + ':' + maxSourceSizeBytes + ':' + priority; + } +} + diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/client/registry/TransformCache.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/registry/TransformCache.java new file mode 100644 index 00000000..bd3a9fff --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/registry/TransformCache.java @@ -0,0 +1,107 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.registry; + +import static java.util.Collections.emptyMap; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class TransformCache +{ + // Looks up supported transform routes given source to target media types. + private final Map>> transforms = + new ConcurrentHashMap<>(); + + // Looks up coreVersion given the transformer name + private final Map coreVersions = new ConcurrentHashMap<>(); + + // Caches results in the ACS repository implementations which repeatedly make the same request. + // Looks up a sorted list of transform routes, for a rendition (if the name is supplied) and the source + // media type. Unlike a lookup on the transforms map above, processing of the transform options and priorities + // will have already been done if cached. + private final Map>> cachedSupportedTransformList = + new ConcurrentHashMap<>(); + + protected int transformerCount = 0; + protected int transformCount = 0; + + public void incrementTransformerCount() + { + transformerCount++; + } + + public void appendTransform(final String sourceMimetype, + final String targetMimetype, final SupportedTransform transform, + String transformerName, String coreVersion) + { + transforms + .computeIfAbsent(sourceMimetype, k -> new ConcurrentHashMap<>()) + .computeIfAbsent(targetMimetype, k -> new ArrayList<>()) + .add(transform); + coreVersions.put(transformerName, coreVersion == null ? "" : coreVersion); + transformCount++; + } + + public Map> retrieveTransforms(final String sourceMimetype) + { + return transforms.getOrDefault(sourceMimetype, emptyMap()); + } + + public Map>> getTransforms() + { + return transforms; + } + + public void cache(final String transformerName, final String sourceMimetype, + final List transformListBySize) + { + cachedSupportedTransformList + .get(transformerName) + .put(sourceMimetype, transformListBySize); + } + + public List retrieveCached(final String transformerName, + final String sourceMimetype) + { + return cachedSupportedTransformList + .computeIfAbsent(transformerName, k -> new ConcurrentHashMap<>()) + .get(sourceMimetype); + } + + @Override + public String toString() + { + return transformerCount == 0 && transformCount == 0 + ? "" + : "(transformers: " + transformerCount + " transforms: " + transformCount + ")"; + } + + public String getCoreVersion(String transformerName) + { + String coreVersion = coreVersions.get(transformerName); + return coreVersion.isBlank() ? null : coreVersion; + } +} + diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/client/registry/TransformRegistryHelper.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/registry/TransformRegistryHelper.java new file mode 100644 index 00000000..7253f7ce --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/registry/TransformRegistryHelper.java @@ -0,0 +1,370 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2019 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.registry; + +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; +import static java.util.Collections.emptySet; +import static java.util.Map.Entry; +import static java.util.stream.Collectors.toMap; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; + +import org.alfresco.transform.client.model.config.TransformOption; +import org.alfresco.transform.client.model.config.TransformOptionGroup; +import org.alfresco.transform.client.model.config.TransformOptionValue; +import org.alfresco.transform.exceptions.TransformException; + +class TransformRegistryHelper +{ + private static final String TIMEOUT = "timeout"; + + static Set lookupTransformOptions(final Set transformOptionNames, + final Map> transformOptions, final String readFrom, + final Consumer logError) + { + if (transformOptionNames == null) + { + return emptySet(); + } + + final Set options = new HashSet<>(); + for (String name : transformOptionNames) + { + final Set oneSetOfTransformOptions = transformOptions.get(name); + if (oneSetOfTransformOptions == null) + { + logError.accept("transformOptions in " + readFrom + " with the name " + name + + " does not exist. Ignored"); + continue; + } + options.add(new TransformOptionGroup(false, oneSetOfTransformOptions)); + } + + return options.size() == 1 ? + ((TransformOptionGroup) options.iterator().next()).getTransformOptions() : + options; + } + + // Returns transformers in increasing supported size order, where lower priority transformers for the same size have + // been discarded. + static List retrieveTransformListBySize(final TransformCache data, + final String sourceMimetype, final String targetMimetype, + Map actualOptions, String transformerName) + { + if (actualOptions == null) + { + actualOptions = emptyMap(); + } + if (transformerName != null && transformerName.trim().isEmpty()) + { + transformerName = null; + } + + final List cachedTransformList = + transformerName == null ? null : + data.retrieveCached(transformerName, sourceMimetype); + if (cachedTransformList != null) + { + return cachedTransformList; + } + + final List builtTransformList = buildTransformList(data, + sourceMimetype, + targetMimetype, + filterTimeout(actualOptions)); + + if (transformerName != null) + { + data.cache(transformerName, sourceMimetype, builtTransformList); + } + + return builtTransformList; + } + + private static List buildTransformList( + final TransformCache data, final String sourceMimetype, final String targetMimetype, + final Map actualOptions) + { + + if(sourceMimetype == null) + { + throw new TransformException(400, "Null value provided for sourceMimetype, please provide a value"); + } + + if(targetMimetype == null) + { + throw new TransformException(400, "Null value provided for tragetMimetype, please provide a value"); + } + + final Map> targetMap = data.retrieveTransforms( + sourceMimetype); + + final List supportedTransformList = targetMap.getOrDefault( + targetMimetype, emptyList()); + + final List transformListBySize = new ArrayList<>(); + + for (SupportedTransform supportedTransform : supportedTransformList) + { + final Map possibleTransformOptions = gatherPossibleTransformOptions( + supportedTransform.getTransformOptions(), actualOptions); + + if (optionsMatch(possibleTransformOptions, actualOptions)) + { + addToSupportedTransformList(transformListBySize, supportedTransform); + } + } + return transformListBySize; + } + + // Add newTransform to the transformListBySize in increasing size order and discards + // lower priority (numerically higher) transforms with a smaller or equal size. + private static void addToSupportedTransformList( + final List transformListBySize, + final SupportedTransform newTransform) + { + if (transformListBySize.isEmpty()) + { + transformListBySize.add(newTransform); + } + else + { + for (int i = 0; i < transformListBySize.size(); i++) + { + final SupportedTransform existingTransform = transformListBySize.get(i); + final int compareMaxSize = compareMaxSize(newTransform.getMaxSourceSizeBytes(), + existingTransform.getMaxSourceSizeBytes()); + final int comparePriority = existingTransform.getPriority() - newTransform.getPriority(); + + if (compareMaxSize == 0) + { + if (comparePriority == 0) + { + // If same priority and size limit, replace with the newer transform. + // It is possibly a replacement in an extension. + transformListBySize.set(i, newTransform); + break; + } + else if (comparePriority > 0) + { + // Replace as newer one is higher priority and try to discard some existing ones. + transformListBySize.set(i, newTransform); + discardFromSupportedTransformList(transformListBySize, i); + break; + } + else + { + // Ignore as lower priority + break; + } + } + else if (compareMaxSize < 0) + { + if (comparePriority > 0) + { + // If higher priority insert and try to discard some existing ones. + transformListBySize.add(i, newTransform); + discardFromSupportedTransformList(transformListBySize, i); + break; + } + else + { + // Ignore the newer one as its priority is lower or the same as one that has a higher size limit + break; + } + } + else // if (compareMaxSize > 0) + { + if (comparePriority < 0) + { + if (i+1 < transformListBySize.size()) + { + // Look at the next element as size is higher but the priority is lower. + continue; + } + else + { + // Append to the list as the size is higher but the priority is lower. + transformListBySize.add(newTransform); + break; + } + } + // Else same or better priority and higher size limit, so replace with the newer transform and try + // to discard some existing ones. + transformListBySize.set(i, newTransform); + discardFromSupportedTransformList(transformListBySize, i); + break; + } + } + } + } + + // Starting at i+1, try to remove transforms that will not be used. + private static void discardFromSupportedTransformList(List transformListBySize, int i) + { + SupportedTransform newTransform = transformListBySize.get(i++); + while (i < transformListBySize.size()) + { + final SupportedTransform existingTransform = transformListBySize.get(i); + final int compareMaxSize = compareMaxSize(newTransform.getMaxSourceSizeBytes(), + existingTransform.getMaxSourceSizeBytes()); + final int comparePriority = existingTransform.getPriority() - newTransform.getPriority(); + + // Discard those with + // 1) the same priority but support a smaller size + // 2) those with a lower priority and a smaller size + if ((comparePriority == 0 && compareMaxSize >= 0) || + (comparePriority > 0 && compareMaxSize >= 0)) + { + transformListBySize.remove(i); + } + else + { + break; + } + } + } + + private static Map gatherPossibleTransformOptions( + final TransformOptionGroup transformOptionGroup, final Map actualOptions) + { + final Map possibleTransformOptions = new HashMap<>(); + addToPossibleTransformOptions(possibleTransformOptions, transformOptionGroup, true, + actualOptions); + return possibleTransformOptions; + } + + /** + * Flatten out the transform options by adding them to the supplied possibleTransformOptions.

    + * + * If possible discards options in the supplied transformOptionGroup if the group is optional and the actualOptions + * don't provide any of the options in the group. Or to put it another way:

    + * + * It adds individual transform options from the transformOptionGroup to possibleTransformOptions if the group is + * required or if the actualOptions include individual options from the group. As a result it is possible that none + * of the group are added if it is optional. It is also possible to add individual transform options that are + * themselves required but not in the actualOptions. In this the optionsMatch method will return false. + * + * @return true if any options were added. Used by nested call parents to determine if an option was added from a + * nested sub group. + */ + static boolean addToPossibleTransformOptions( + final Map possibleTransformOptions, + final TransformOptionGroup transformOptionGroup, final Boolean parentGroupRequired, + final Map actualOptions) + { + boolean added = false; + boolean required = false; + + final Set optionList = transformOptionGroup.getTransformOptions(); + if (optionList != null && !optionList.isEmpty()) + { + // We need to avoid adding options from a group that is required but its parents are not. + boolean transformOptionGroupRequired = transformOptionGroup.isRequired() && parentGroupRequired; + + // Check if the group contains options in actualOptions. This will add any options from sub groups. + for (final TransformOption transformOption : optionList) + { + if (transformOption instanceof TransformOptionGroup) + { + added = addToPossibleTransformOptions(possibleTransformOptions, + (TransformOptionGroup) transformOption, transformOptionGroupRequired, + actualOptions); + required |= added; + } + else + { + final String name = ((TransformOptionValue) transformOption).getName(); + if (actualOptions.containsKey(name)) + { + required = true; + } + } + } + + if (required || transformOptionGroupRequired) + { + for (TransformOption transformOption : optionList) + { + if (transformOption instanceof TransformOptionValue) + { + added = true; + final TransformOptionValue option = (TransformOptionValue) transformOption; + possibleTransformOptions.put(option.getName(), option.isRequired()); + } + } + } + } + + return added; + } + + // compare where -1 is unlimited. + private static int compareMaxSize(final long a, final long b) + { + return a == -1 ? b == -1 ? 0 : 1 : b == -1 ? -1 : Long.compare(a, b); + } + + static boolean optionsMatch(final Map transformOptions, + final Map actualOptions) + { + // Check all required transformOptions are supplied + final boolean supported = transformOptions + .entrySet() + .stream() + .filter(Entry::getValue)// filter by the required status + .map(Entry::getKey)// map to the option name + .allMatch(actualOptions::containsKey); + + if (!supported) + { + return false; + } + + // Check there are no extra unused actualOptions + return actualOptions + .keySet() + .stream() + .allMatch(transformOptions::containsKey); + } + + private static Map filterTimeout(final Map options) + { + // Remove the "timeout" property from the actualOptions as it is not used to select a transformer. + if (!options.containsKey(TIMEOUT)) + { + return options; + } + return options + .entrySet() + .stream() + .filter(e -> !TIMEOUT.equals(e.getKey())) + .collect(toMap(Entry::getKey, Entry::getValue)); + } +} diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/client/registry/TransformServiceRegistry.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/registry/TransformServiceRegistry.java new file mode 100644 index 00000000..30f5a7d6 --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/registry/TransformServiceRegistry.java @@ -0,0 +1,103 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.registry; + +import org.alfresco.transform.client.model.config.CoreFunction; + +import java.util.Map; + +/** + * Used by clients work out if a transformation is supported by a Transform Service. + */ +public interface TransformServiceRegistry +{ + /** + * Works out if the Transform Server should be able to transform content of a given source mimetype and size into a + * target mimetype given a list of actual transform option names and values (Strings) plus the data contained in the + * Transformer objects registered with this class. + * + * @param sourceMimetype the mimetype of the source content + * @param sourceSizeInBytes the size in bytes of the source content. Ignored if negative. + * @param targetMimetype the mimetype of the target + * @param actualOptions the actual name value pairs available that could be passed to the Transform Service. + * @param transformName (optional) name for the set of options and target mimetype. If supplied is used to cache + * results to avoid having to work out if a given transformation is supported a second time. + * The sourceMimetype and sourceSizeInBytes may still change. In the case of ACS this is the + * rendition name. + * @return {@code}true{@code} if it is supported. + */ + default boolean isSupported(final String sourceMimetype, final long sourceSizeInBytes, + final String targetMimetype, final Map actualOptions, + final String transformName) + { + long maxSize = findMaxSize(sourceMimetype, targetMimetype, actualOptions, transformName); + return maxSize != 0 && (maxSize == -1L || maxSize >= sourceSizeInBytes); + } + + /** + * Returns the maximun size (in bytes) of the source content that can be transformed. + * + * @param sourceMimetype the mimetype of the source content + * @param targetMimetype the mimetype of the target + * @param actualOptions the actual name value pairs available that could be passed to the Transform Service. + * @param transformName (optional) name for the set of options and target mimetype. If supplied is used to cache + * results to avoid having to work out if a given transformation is supported a second time. + * The sourceMimetype and sourceSizeInBytes may still change. In the case of ACS this is the + * rendition name. + * @return the maximum size (in bytes) of the source content that can be transformed. If {@code -1} there is no + * limit, but if {@code 0} the transform is not supported. + */ + long findMaxSize(String sourceMimetype, String targetMimetype, Map actualOptions, + String transformName); + + /** + * Works out the name of the transformer (might not map to an actual transformer) that will be used to transform + * content of a given source mimetype and size into a target mimetype given a list of actual transform option names + * and values (Strings) plus the data contained in the Transformer objects registered with this class. + * + * @param sourceMimetype the mimetype of the source content + * @param sourceSizeInBytes the size in bytes of the source content. Ignored if negative. + * @param targetMimetype the mimetype of the target + * @param actualOptions the actual name value pairs available that could be passed to the Transform Service. + * @param renditionName (optional) name for the set of options and target mimetype. If supplied is used to cache + * results to avoid having to work out if a given transformation is supported a second time. + * The sourceMimetype and sourceSizeInBytes may still change. In the case of ACS this is the + * rendition name. + * @return the name of the transformer or {@code}null{@code} if not set or there is no supported transformer. + */ + String findTransformerName(String sourceMimetype, long sourceSizeInBytes, String targetMimetype, + Map actualOptions, String renditionName); + + /** + * Returns {@code true} if the {@code function} is supported by the named transformer. Not all transformers are + * able to support all functionality, as newer features may have been introduced into the core t-engine code since + * it was released. Normally used in conjunction with {@link #findTransformerName(String, long, String, Map, String)} + * rather than {@link #isSupported(String, long, String, Map, String)}. + * @param function to be checked. + * @param transformerName name of the transformer. + * @return {@code true} is supported, {@code false} otherwise. + */ + default boolean isSupported(CoreFunction function, String transformerName) + { + return false; + } +} diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/client/registry/TransformerAndSourceType.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/registry/TransformerAndSourceType.java new file mode 100644 index 00000000..9d03f863 --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/registry/TransformerAndSourceType.java @@ -0,0 +1,84 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2021 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.registry; + +import org.alfresco.transform.client.model.config.OverrideSupported; + +import java.util.Objects; + +/** + * Key based using {@code transformerName} and {@code sourceMediaType} used as a key to lookup default values held in + * {@link OverrideSupported} objects. + */ +class TransformerAndSourceType +{ + private String transformerName; + private String sourceMediaType; + + public TransformerAndSourceType(String transformerName, String sourceMediaType) + { + this.transformerName = transformerName; + this.sourceMediaType = sourceMediaType; + } + + public void setTransformerName(String transformerName) + { + this.transformerName = transformerName; + } + + public void setSourceMediaType(String sourceMediaType) + { + this.sourceMediaType = sourceMediaType; + } + + public String getTransformerName() + { + return transformerName; + } + + public String getSourceMediaType() + { + return sourceMediaType; + } + + @Override + public boolean equals(Object o) + { + if (this == o) + { + return true; + } + if (o == null || getClass() != o.getClass()) + { + return false; + } + TransformerAndSourceType that = (TransformerAndSourceType)o; + return Objects.equals(transformerName, that.transformerName) && + Objects.equals(sourceMediaType, that.sourceMediaType); + } + + @Override + public int hashCode() + { + return Objects.hash(transformerName, sourceMediaType); + } +} diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/client/util/RequestParamMap.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/util/RequestParamMap.java new file mode 100644 index 00000000..3bd59f4a --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/client/util/RequestParamMap.java @@ -0,0 +1,77 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.util; + +import org.alfresco.transform.client.model.config.CoreVersionDecorator; + +/** + * Request parameters and transform options used in the core transformers. + */ +public interface RequestParamMap +{ + // Transform options used in the core transformers. + String SOURCE_ENCODING = "sourceEncoding"; + String TARGET_ENCODING = "targetEncoding"; + String PAGE_REQUEST_PARAM = "page"; + String WIDTH_REQUEST_PARAM = "width"; + String HEIGHT_REQUEST_PARAM = "height"; + String ALLOW_PDF_ENLARGEMENT = "allowPdfEnlargement"; + String MAINTAIN_PDF_ASPECT_RATIO = "maintainPdfAspectRatio"; + String START_PAGE = "startPage"; + String END_PAGE = "endPage"; + String ALPHA_REMOVE = "alphaRemove"; + String AUTO_ORIENT = "autoOrient"; + String CROP_GRAVITY = "cropGravity"; + String CROP_WIDTH = "cropWidth"; + String CROP_HEIGHT = "cropHeight"; + String CROP_PERCENTAGE = "cropPercentage"; + String CROP_X_OFFSET = "cropXOffset"; + String CROP_Y_OFFSET = "cropYOffset"; + String THUMBNAIL = "thumbnail"; + String RESIZE_WIDTH = "resizeWidth"; + String RESIZE_HEIGHT = "resizeHeight"; + String RESIZE_PERCENTAGE = "resizePercentage"; + String ALLOW_ENLARGEMENT = "allowEnlargement"; + String MAINTAIN_ASPECT_RATIO = "maintainAspectRatio"; + String COMMAND_OPTIONS = "commandOptions"; + String TIMEOUT = "timeout"; + String INCLUDE_CONTENTS = "includeContents"; + String NOT_EXTRACT_BOOKMARKS_TEXT = "notExtractBookmarksText"; + String PAGE_LIMIT = "pageLimit"; + + // Parameters interpreted by the AbstractTransformerController + String DIRECT_ACCESS_URL = "directAccessUrl"; + + // An optional parameter (defaults to 1) to be included in the request to the t-engine {@code /transform/config} + // endpoint to specify what version (of the schema) to return. Provides the flexibility to introduce changes + // without getting deserialization issues when we have components at different versions. + String CONFIG_VERSION = "configVersion"; + String CONFIG_VERSION_DEFAULT = "1"; + int CONFIG_VERSION_LATEST = CoreVersionDecorator.CONFIG_VERSION_INCLUDES_CORE_VERSION; + + // Endpoints + String ENDPOINT_TRANSFORM = "/transform"; + String ENDPOINT_TRANSFORM_CONFIG = "/transform/config"; + String ENDPOINT_TRANSFORM_CONFIG_LATEST = ENDPOINT_TRANSFORM_CONFIG + "?" + CONFIG_VERSION + "=" + CONFIG_VERSION_LATEST; + String ENDPOINT_TRANSFORM_LOG = "/log"; + String ENDPOINT_TRANSFORM_TEST = "/"; +} diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/exceptions/TransformException.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/exceptions/TransformException.java new file mode 100644 index 00000000..dad82c2f --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/exceptions/TransformException.java @@ -0,0 +1,44 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2018 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.exceptions; + +public class TransformException extends RuntimeException +{ + private final int statusCode; + + public TransformException(int statusCode, String message) + { + super(message); + this.statusCode = statusCode; + } + + public TransformException(int statusCode, String message, Throwable cause) + { + super(message, cause); + this.statusCode = statusCode; + } + + public int getStatusCode() + { + return statusCode; + } +} diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/router/ExtensionService.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/router/ExtensionService.java new file mode 100644 index 00000000..04e90ebe --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/router/ExtensionService.java @@ -0,0 +1,155 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.router; + +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_DITA; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_EXCEL; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_IMAGE_SVG; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_OPENDOCUMENT_GRAPHICS; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_OPENDOCUMENT_PRESENTATION; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_OPENDOCUMENT_PRESENTATION_TEMPLATE; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_OPENDOCUMENT_SPREADSHEET; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_OPENDOCUMENT_SPREADSHEET_TEMPLATE; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_OPENDOCUMENT_TEXT; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_OPENDOCUMENT_TEXT_TEMPLATE; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_OPENOFFICE1_CALC; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_OPENOFFICE1_IMPRESS; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_OPENOFFICE1_WRITER; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_OPENXML_PRESENTATION; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_OPENXML_PRESENTATION_ADDIN; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_OPENXML_PRESENTATION_MACRO; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_OPENXML_PRESENTATION_SLIDE; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_OPENXML_PRESENTATION_SLIDESHOW; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_OPENXML_PRESENTATION_SLIDE_MACRO; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_OPENXML_PRESENTATION_TEMPLATE; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_OPENXML_PRESENTATION_TEMPLATE_MACRO; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_OPENXML_SPREADSHEET; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_OPENXML_SPREADSHEET_BINARY_MACRO; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_OPENXML_SPREADSHEET_MACRO; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_OPENXML_SPREADSHEET_TEMPLATE; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_OPENXML_SPREADSHEET_TEMPLATE_MACRO; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_OPENXML_WORDPROCESSING; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_OPENXML_WORDPROCESSING_MACRO; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_OPENXML_WORD_TEMPLATE; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_OPENXML_WORD_TEMPLATE_MACRO; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_OUTLOOK_MSG; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_PPT; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_VISIO; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_VISIO_2013; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_WORD; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_WORDPERFECT; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_TEXT_PLAIN; +import static org.alfresco.transform.router.TransformerDebug.MIMETYPE_METADATA_EMBED; +import static org.alfresco.transform.router.TransformerDebug.MIMETYPE_METADATA_EXTRACT; + +import java.util.Map; + +/** + * Provides mapping between mimtypes and file extensions, static and not configurable. + * The correct extension is required for a small subset of transforms in pipelines which go through the + * libreoffice transformer + */ +public class ExtensionService +{ + private final static String MIMETYPE_TAB_SEPARATED_VALUES="text/tab-separated-values"; + private final static String MIMETYPE_CALC_TEMPLATE="application/vnd.sun.xml.calc.template"; + private final static String MIMETYPE_IMPRESS_TEMPLATE="application/vnd.sun.xml.impress.template"; + private final static String MIMETYPE_WRITER_TEMPLATE="application/vnd.sun.xml.writer.template"; + + private static final Map mimetpeExtensions = Map.ofEntries( + Map.entry(MIMETYPE_WORD, "doc"), + Map.entry(MIMETYPE_OPENXML_WORDPROCESSING_MACRO, "docm"), + Map.entry(MIMETYPE_OPENXML_WORDPROCESSING, "docx"), + Map.entry(MIMETYPE_OPENXML_WORD_TEMPLATE_MACRO, "dotm"), + Map.entry(MIMETYPE_OPENXML_WORD_TEMPLATE, "dotx"), + Map.entry(MIMETYPE_OPENDOCUMENT_GRAPHICS, "odg"), + Map.entry(MIMETYPE_OPENDOCUMENT_PRESENTATION, "odp"), + Map.entry(MIMETYPE_OPENDOCUMENT_PRESENTATION_TEMPLATE, "otp"), + Map.entry(MIMETYPE_OPENDOCUMENT_SPREADSHEET, "ods"), + Map.entry(MIMETYPE_OPENDOCUMENT_SPREADSHEET_TEMPLATE, "ots"), + Map.entry(MIMETYPE_OPENDOCUMENT_TEXT, "odt"), + Map.entry(MIMETYPE_OPENDOCUMENT_TEXT_TEMPLATE, "ott"), + Map.entry(MIMETYPE_OPENXML_PRESENTATION_TEMPLATE_MACRO, "potm"), + Map.entry(MIMETYPE_OPENXML_PRESENTATION_TEMPLATE, "potx"), + Map.entry(MIMETYPE_OPENXML_PRESENTATION_ADDIN, "ppam"), + Map.entry(MIMETYPE_PPT, "ppt"), + Map.entry(MIMETYPE_OPENXML_PRESENTATION_MACRO, "pptm"), + Map.entry(MIMETYPE_OPENXML_PRESENTATION, "pptx"), + Map.entry(MIMETYPE_OPENXML_PRESENTATION_SLIDE_MACRO, "sldm"), + Map.entry(MIMETYPE_OPENXML_PRESENTATION_SLIDE, "sldx"), + Map.entry(MIMETYPE_CALC_TEMPLATE, "stc"), + Map.entry(MIMETYPE_IMPRESS_TEMPLATE, "sti"), + Map.entry(MIMETYPE_WRITER_TEMPLATE, "stw"), + Map.entry(MIMETYPE_TAB_SEPARATED_VALUES, "tsv"), + Map.entry(MIMETYPE_OPENOFFICE1_CALC, "sxc"), + Map.entry(MIMETYPE_OPENOFFICE1_IMPRESS, "sxi"), + Map.entry(MIMETYPE_OPENOFFICE1_WRITER, "sxw"), + Map.entry(MIMETYPE_VISIO, "vsd"), + Map.entry(MIMETYPE_VISIO_2013, "vsdx"), + Map.entry(MIMETYPE_WORDPERFECT, "wp"), + Map.entry(MIMETYPE_EXCEL, "xls"), + Map.entry(MIMETYPE_OPENXML_SPREADSHEET_BINARY_MACRO, "xlsb"), + Map.entry(MIMETYPE_OPENXML_SPREADSHEET_MACRO, "xlsm"), + Map.entry(MIMETYPE_OPENXML_SPREADSHEET, "xlsx"), + Map.entry(MIMETYPE_OPENXML_SPREADSHEET_TEMPLATE_MACRO, "xltm"), + Map.entry(MIMETYPE_OPENXML_PRESENTATION_SLIDESHOW, "ppsx"), + Map.entry(MIMETYPE_OUTLOOK_MSG, "msg"), + Map.entry(MIMETYPE_DITA, "dita"), + Map.entry(MIMETYPE_OPENXML_SPREADSHEET_TEMPLATE, "xltx"), + Map.entry(MIMETYPE_IMAGE_SVG, "svg"), + Map.entry(MIMETYPE_TEXT_PLAIN, "txt") + ); + + public static String getExtensionForTargetMimetype(String targetMimetype, String sourceMimetype) + { + if (targetMimetype == null) + { + return null; + } + return getExtensionForMimetype(MIMETYPE_METADATA_EMBED.equals(targetMimetype) ? sourceMimetype : targetMimetype); + } + + public static String getExtensionForMimetype(String mimetype) + { + if (mimetype == null) + { + return null; + } + if (mimetpeExtensions.containsKey(mimetype)) + { + return mimetpeExtensions.get(mimetype); + } + if (MIMETYPE_METADATA_EXTRACT.equals(mimetype)) + { + return "json"; + } + // else fall back to the original implementation + return splitMimetype(mimetype); + } + + // Moved from Dispatcher. This fails to work in many cases, but we need to be backward compatible. + private static String splitMimetype(final String mimetype) + { + final String[] parts = mimetype.split("[.\\-_|/]"); + return parts[parts.length - 1]; + } +} diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/router/RepositoryClientData.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/router/RepositoryClientData.java new file mode 100644 index 00000000..4f4606ea --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/router/RepositoryClientData.java @@ -0,0 +1,97 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.router; + +import java.util.StringJoiner; +import java.util.stream.Stream; + +/** + * The client data supplied and echoed back to content repository (the client). May be modified to include + * TransformerDebug. + */ +class RepositoryClientData +{ + private static final String CLIENT_DATA_SEPARATOR = "\u23D0"; + public static final String DEBUG_SEPARATOR = "\u23D1"; + + private static final String REPO_ID = "Repo"; + private static final String DEBUG = "debug:"; + + private final String[] split; + + public RepositoryClientData(String clientData) + { + split = clientData == null ? null : clientData.split(CLIENT_DATA_SEPARATOR); + } + + private boolean isRepositoryClientData() + { + return split != null && split.length == 10 && split[0].startsWith(REPO_ID); + } + + public String getAcsVersion() + { + return isRepositoryClientData() ? split[0].substring(REPO_ID.length()) : ""; + } + + public int getRequestId() + { + try + { + return isRepositoryClientData() ? Integer.parseInt(split[6]) : -1; + } + catch (NumberFormatException e) + { + return -1; + } + } + + public String getRenditionName() + { + return isRepositoryClientData() ? split[2] : ""; + } + + public void appendDebug(String message) + { + if (isDebugRequested()) + { + split[9] += DEBUG_SEPARATOR+ message; + } + } + + public boolean isDebugRequested() + { + return isRepositoryClientData() && split[9].startsWith(DEBUG); + } + + @Override + public String toString() + { + if (split == null) + { + return null; + } + StringJoiner sj = new StringJoiner(CLIENT_DATA_SEPARATOR); + Stream.of(split).forEach(element -> sj.add(element)); + return sj.toString(); + } +} diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/router/TransformStack.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/router/TransformStack.java new file mode 100644 index 00000000..ba9f3672 --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/router/TransformStack.java @@ -0,0 +1,546 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.router; + +import org.alfresco.transform.client.model.InternalContext; +import org.alfresco.transform.client.model.TransformReply; +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.StringJoiner; + +/** + * Represents the current state of a top level transform request in terms of its current nested call stack, which is + * the current transform step being performed and what other transform steps are still to be executed in the current + * level. This information is encoded in the {@link org.alfresco.transform.client.model.MultiStep} structure of the + * internal context passed between T-Router and T-Engines. Ideally we would have changed the structure, + * but for backward compatibility we are using the existing structure, which allows T-Engines that were developed + * previously to be used unchanged.


    + * + * Originally the T-Router only allowed pipeline and single step transforms, so it was possible to represent them as a + * flat list. However the original design was of a nested structure. Pipelines are just one example, with failover + * transforms being the other. To bring the T-Router up to the same level of functionality as the Repository, it too + * needs to support nested transforms.


    + * + *

  • @{code transformsToBeDone[0]} holds the original Transformer Request Options as a list of key values pairs. + * Needed so that we don't lose values as we walk down the individual transform steps
  • + *
  • @{code transformsToBeDone[1]} holds the original source reference so that we don't delete the original source + * until the whole transform is known to be successful, so that a queue entry may be retried on failure.
  • + *
  • @{code transformsToBeDone[2]} holds information about the top level transform
  • + *
  • @{code transformsToBeDone[size()-1]} holds information about the most nested transform being processed
  • + *
  • Each level contains a list of step transforms. Just one for a singe step transform or a list for pipeline and + * failover transforms
  • + *
  • When a step is processed it will result in the creation of another level if it is a pipeline or failover + * transform
  • + *
  • As steps are completed, they are removed
  • + *
  • When there are no steps left in a level the level is removed
  • + * + * Each level is represented by a String with a pipeline or failover flag @{code 'P'|'F'} followed by a step counter + * and start time used in debug, a retry count and a sequence of transform steps. Each step is made up of three parts: + * @{code|| . All fields are separated by a @code{'\u23D0'} character. + * The last step in the sequence is the current transform being performed. The top level transform is a pipeline of + * one step. Although the source and target mimetypes are always the same for failover transforms, they use the same + * structure. + */ +public class TransformStack +{ + public static final String PIPELINE_FLAG = "P"; + public static final String FAILOVER_FLAG = "F"; + + static final String SEPARATOR = "\u23D0"; + private static final String SEPARATOR_REGEX = "\u23D0"; + + static final int OPTIONS_LEVEL = 0; + static final int SOURCE_REFERENCE_LEVEL = 1; + static final int TOP_STACK_LEVEL = 2; + + private static final int FLAG_INDEX = 0; + private static final int REFERENCE_INDEX = 1; + private static final int START_INDEX = 2; + private static final int RETRY_INDEX = 3; + + private static final int FIELDS_IN_HEADER = 4; // flag | counter | retry + private static final int FIELDS_PER_STEP = 3; // name | source | target + + public static LevelBuilder levelBuilder(String flag) + { + return new LevelBuilder(flag); + } + + public static class LevelBuilder + { + private final ArrayList reverseOrderStepElements = new ArrayList<>(); + private final String flag; + + private LevelBuilder(String flag) + { + this.flag = flag; + } + + public LevelBuilder withStep(String transformerName, String sourceMediaType, String targetMediaType) + { + reverseOrderStepElements.add(targetMediaType); + reverseOrderStepElements.add(sourceMediaType); + reverseOrderStepElements.add(transformerName); + return this; + } + + public String build() + { + StringJoiner stringJoiner = new StringJoiner(SEPARATOR); + stringJoiner.add(flag); + stringJoiner.add("1"); + stringJoiner.add("0"); + stringJoiner.add("0"); + for (int i=reverseOrderStepElements.size()-1; i>=0; i--) + { + stringJoiner.add(reverseOrderStepElements.get(i)); + } + return stringJoiner.toString(); + } + } + + public static class Step + { + private final String[] step; + + private Step(String stepWithSeparatedFields) + { + this.step = stepWithSeparatedFields.split(SEPARATOR_REGEX); + } + + public String getTransformerName() + { + return step[0]; + } + + public String getSourceMediaType() + { + return step[1]; + } + + public String getTargetMediaType() + { + return step[2]; + } + } + + public static void setInitialTransformRequestOptions(InternalContext internalContext, + Map transformRequestOptions) + { + init(internalContext); + StringJoiner sj = new StringJoiner(SEPARATOR); + transformRequestOptions.forEach((key,value)-> sj.add(key).add(value)); + levels(internalContext).set(OPTIONS_LEVEL, sj.toString()); + } + + public static void setInitialSourceReference(InternalContext internalContext, String sourceReference) + { + init(internalContext); + levels(internalContext).set(SOURCE_REFERENCE_LEVEL, sourceReference); + } + + public static Map getInitialTransformRequestOptions(InternalContext internalContext) + { + Map transformRequestOptions = new HashMap<>(); + + // To avoid the case where split() discards the last value, when it is a zero length string, we add and remove + // a space. None of the keys or value may be null. + String[] split = (level(internalContext, OPTIONS_LEVEL)+' ').split(SEPARATOR_REGEX); + String lastValue = split[split.length - 1]; + split[split.length-1] = lastValue.substring(0, lastValue.length()-1); + + for (int i = split.length-2; i >= 0; i-=2) + { + transformRequestOptions.put(split[i], split[i+1]); + } + return transformRequestOptions; + } + + public static String getInitialSourceReference(InternalContext internalContext) + { + return level(internalContext, SOURCE_REFERENCE_LEVEL); + } + + private static List levels(InternalContext internalContext) + { + return internalContext.getMultiStep().getTransformsToBeDone(); + } + + private static String level(InternalContext internalContext, int i) + { + return levels(internalContext).get(i); + } + + private static void init(InternalContext internalContext) + { + while(levels(internalContext).size() < TOP_STACK_LEVEL) + { + levels(internalContext).add(null); + } + } + + private static String currentLevel(InternalContext internalContext) + { + return parentLevel(internalContext, 0); + } + + private static String parentLevel(InternalContext internalContext, int parentLevels) + { + List levels = levels(internalContext); + int i = levels.size() - 1 - parentLevels; + return i >= TOP_STACK_LEVEL ? levels.get(i) : null; + } + + public static boolean isFinished(InternalContext internalContext) + { + + int levelsLeft = levels(internalContext).size() - TOP_STACK_LEVEL; + return levelsLeft <= 0 || // there has been an error, so we have lost the stack + levelsLeft == 1 && // on top level wrapper level + isTransformLevelFinished(internalContext); // the one step has been processed (removed) + + } + + public static void addTransformLevel(InternalContext internalContext, LevelBuilder levelBuilder) + { + levels(internalContext).add(levelBuilder.build()); + } + + public static void setReference(InternalContext internalContext, int requestCountOrClientRequestId) + { + setHeaderField(internalContext, REFERENCE_INDEX, requestCountOrClientRequestId); + } + + public static void incrementReference(InternalContext internalContext) + { + setHeaderField(internalContext, REFERENCE_INDEX, getReferenceCounter(internalContext)+1); + } + + public static void resetAttemptedRetries(InternalContext internalContext) + { + setHeaderField(internalContext, RETRY_INDEX, 0); + } + + public static void setStartTime(InternalContext internalContext) + { + setHeaderField(internalContext, START_INDEX, System.currentTimeMillis()); + } + + public static void incrementAttemptedRetries(InternalContext internalContext) + { + setHeaderField(internalContext, RETRY_INDEX,getAttemptedRetries(internalContext)+1); + } + + private static void setHeaderField(InternalContext internalContext, int index, long value) + { + List levels = levels(internalContext); + int size = levels.size(); + String level = levels.get(size-1); + int j = indexOfField(level, index); + int k = level.indexOf(SEPARATOR, j+1); + levels.set(size-1, level.substring(0, j) + value + level.substring(k)); + } + + public static String getReference(InternalContext internalContext) + { + StringJoiner ref = new StringJoiner("."); + List levels = levels(internalContext); + for (int i=TOP_STACK_LEVEL; i levels = levels(internalContext); + levels.remove(levels.size()-1); + } + + public static void removeRemainingTransformLevels(TransformReply reply, TransformerDebug transformerDebug) + { + List levels = levels(reply.getInternalContext()); + if (levels != null) + { + while (!TransformStack.isFinished(reply.getInternalContext())) + { + removeFailedStep(reply, transformerDebug); + } + } + } + + public static boolean isParentAFailover(InternalContext internalContext) + { + String level = currentLevel(internalContext); + return level != null && level.startsWith(FAILOVER_FLAG); + } + + public static String getParentName(InternalContext internalContext) + { + String level = parentLevel(internalContext, 1); + return level == null ? null : new Step(level.substring(indexOfNextStep(level))).getTransformerName(); + } + + public static Step currentStep(InternalContext internalContext) + { + String level = currentLevel(internalContext); + return new Step(level.substring(indexOfNextStep(level))); + } + + public static boolean isLastStepInTransformLevel(InternalContext internalContext) + { + return getStepCount(internalContext) == 1; + } + + private static boolean isTransformLevelFinished(InternalContext internalContext) + { + return getStepCount(internalContext) == 0; + } + + private static int getStepCount(InternalContext internalContext) + { + return (StringUtils.countMatches(currentLevel(internalContext), SEPARATOR)+1-FIELDS_IN_HEADER)/FIELDS_PER_STEP; + } + + public static void removeSuccessfulStep(TransformReply reply, TransformerDebug transformerDebug) + { + removeFinishedSteps(reply, true, transformerDebug); + } + + public static void removeFailedStep(TransformReply reply, TransformerDebug transformerDebug) + { + removeFinishedSteps(reply, false, transformerDebug); + } + + private static void removeFinishedSteps(TransformReply reply, boolean successful, + TransformerDebug transformerDebug) + { + TransformStack.removeStep(reply, successful, transformerDebug); + + InternalContext internalContext = reply.getInternalContext(); + while (!TransformStack.isFinished(internalContext) && + TransformStack.isTransformLevelFinished(internalContext)) + { + TransformStack.removeTransformLevel(internalContext); + // We want to exit if removing steps for a failure and we have a parent that is a failover transform + if (successful || !TransformStack.isParentAFailover(internalContext)) + { + TransformStack.removeStep(reply, successful, transformerDebug); + } + } + } + + private static void removeStep(TransformReply reply, boolean successful, TransformerDebug transformerDebug) + { + InternalContext internalContext = reply.getInternalContext(); + boolean parentAFailover = isParentAFailover(internalContext); + boolean successfulFailoverStep = successful && parentAFailover; + boolean unsuccessfulPipelineStep = !successful && !parentAFailover; + + transformerDebug.popTransform(reply); + + // For a successful failover step, or an unsuccessful pipeline step remove all sibling steps, otherwise just + // remove one step as it was a successful pipeline step or an unsuccessful failover step + List levels = levels(internalContext); + int size = levels.size(); + String level = levels.get(size-1); + levels.set(size-1, level.substring(0, + (successfulFailoverStep || unsuccessfulPipelineStep ? indexOfLastStep(level) : indexOfNextStep(level)) - 1)); + + if (!isTransformLevelFinished(internalContext)) + { + TransformStack.incrementReference(internalContext); + } + } + + private static int indexOfNextStep(String level) + { + int j = level.length()-1; + for (int i = FIELDS_PER_STEP; i >= 1 && j > 0; i--) + { + j = level.lastIndexOf(SEPARATOR, j-1); + } + return j+1; + } + + private static int indexOfLastStep(String level) + { + return indexOfField(level, FIELDS_IN_HEADER); + } + + private static int indexOfField(String level, int n) + { + int j = 0; + for (int i = n; i >= 1; i--) + { + j = level.indexOf(SEPARATOR, j+1); + } + return j+1; + } + + public static String checkStructure(InternalContext internalContext, String type) + { + // A null value will have been replaced with an empty array, so no need to check for that. + String errorMessage = levels(internalContext).size() < (TOP_STACK_LEVEL + 1) + ? type+" InternalContext did not have the Stack set" + : !validTransformOptions(internalContext) + ? type+" InternalContext did not have the TransformOptions set correctly" + : levels(internalContext).size() == 1 + ? type+" InternalContext levels were not set" + : !validLevels(levels(internalContext)) + ? type+" InternalContext did not have levels set correctly" + : null; + return errorMessage; + } + + private static boolean validTransformOptions(InternalContext internalContext) + { + String keysAndValues = level(internalContext, OPTIONS_LEVEL); + if (keysAndValues == null) + { + return false; + } + if ("".equals(keysAndValues)) + { + return true; + } + String[] split = keysAndValues.split(SEPARATOR_REGEX); + if (split.length%2 != 0) + { + return false; + } + for (int i = split.length-2; i >= 0; i-=2) + { + if (split[i].isEmpty()) + { + return false; + } + } + return true; + } + + private static boolean validLevels(List levels) + { + for (int i=levels.size()-1; i >=TOP_STACK_LEVEL; i--) + { + String level = levels.get(i); + if (!validLevel(level)) + { + return false; + } + } + return true; + } + + private static boolean validLevel(String level) + { + if (level == null) + { + return false; + } + String[] split = level.split(SEPARATOR_REGEX); + if (split.length < FIELDS_IN_HEADER+FIELDS_PER_STEP || // must be at least 1 step + (split.length-FIELDS_IN_HEADER)%FIELDS_PER_STEP != 0 || + (!PIPELINE_FLAG.equals(split[FLAG_INDEX]) && + !FAILOVER_FLAG.equals(split[FLAG_INDEX])) || + !aPositiveInt(split[REFERENCE_INDEX]) || + !aPositiveLong(split[START_INDEX]) || + !aPositiveInt(split[RETRY_INDEX])) + { + return false; + } + + for (int i=split.length-1; i>=FIELDS_IN_HEADER; i--) + { + if (split[i].isBlank()) + { + return false; + } + } + + return true; + } + + private static boolean aPositiveInt(String string) + { + try + { + return Integer.valueOf(string).toString().equals(string) && Integer.valueOf(string) >= 0; + } + catch (NumberFormatException e) + { + return false; + } + } + + private static boolean aPositiveLong(String string) + { + try + { + return Long.valueOf(string).toString().equals(string) && Long.valueOf(string) >= 0; + } + catch (NumberFormatException e) + { + return false; + } + } +} diff --git a/alfresco-transform-model/src/main/java/org/alfresco/transform/router/TransformerDebug.java b/alfresco-transform-model/src/main/java/org/alfresco/transform/router/TransformerDebug.java new file mode 100644 index 00000000..202ac56b --- /dev/null +++ b/alfresco-transform-model/src/main/java/org/alfresco/transform/router/TransformerDebug.java @@ -0,0 +1,345 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.router; + +import org.alfresco.transform.client.model.InternalContext; +import org.alfresco.transform.client.model.TransformReply; +import org.alfresco.transform.client.model.TransformRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.Map; + +public class TransformerDebug +{ + public static final Logger logger = LoggerFactory.getLogger(TransformerDebug.class); + + private static final int REFERENCE_SIZE = 15; + + private static final String TRANSFORM_NAMESPACE = "transform:"; + + public static final String MIMETYPE_METADATA_EXTRACT = "alfresco-metadata-extract"; + + public static final String MIMETYPE_METADATA_EMBED = "alfresco-metadata-embed"; + + private static final String TIMEOUT = "timeout"; + + // For truncating long option values + private static int MAX_OPTION_VALUE = 60; + private static int MAX_OPTION_END_CHARS = 5; + private static String MAX_OPTION_DOTS = "..."; + + private boolean isTEngine = false; + + public void pushTransform(TransformRequest request) + { + RepositoryClientData repositoryClientData = new RepositoryClientData(request.getClientData()); + if (isEnabled(repositoryClientData)) + { + TransformStack.Step step = TransformStack.currentStep(request.getInternalContext()); + String reference = TransformStack.getReference(request.getInternalContext()); + boolean isTopLevel = isTopLevel(reference); + + String message = getPaddedReference(reference) + + getMimetypeExt(step.getSourceMediaType()) + + getTargetMimetypeExt(step.getTargetMediaType(), step.getSourceMediaType()) + ' ' + + (isTopLevel || isTEngine() + ? fileSize(request.getSourceSize()) + ' ' + + getRenditionName(new RepositoryClientData(request.getClientData()).getRenditionName()) + : "") + + step.getTransformerName(); + if (isDebugToBeReturned(repositoryClientData)) + { + repositoryClientData.appendDebug(message); + request.setClientData(repositoryClientData.toString()); + } + logger.debug(message); + } + } + + public void pushTransform(String reference, String sourceMimetype, String targetMimetype, File sourceFile, String transformerName) + { + if (logger.isDebugEnabled()) + { + final long sourceSizeInBytes = sourceFile.length(); + String message = getPaddedReference(reference) + + getMimetypeExt(sourceMimetype) + + getTargetMimetypeExt(targetMimetype, sourceMimetype) + ' ' + + fileSize(sourceSizeInBytes) + ' ' + + transformerName; + logger.debug(message); + } + } + + public void popTransform(TransformReply reply) + { + if (logger.isDebugEnabled()) + { + InternalContext internalContext = reply.getInternalContext(); + String reference = TransformStack.getReference(internalContext); + long elapsedTime = TransformStack.getElapsedTime(internalContext); + + String message = getPaddedReference(reference) + "Finished in " + ms(elapsedTime); + if (isTopLevel(reference) || isTEngine()) + { + logger.debug(message); + } + else + { + logger.trace(message); + } + // We don't append the Finished message to ClientData as that would be too much + } + } + + public void popTransform(String reference, long elapsedTime) + { + if (logger.isDebugEnabled()) + { + String message = getPaddedReference(reference) + "Finished in " + ms(elapsedTime); + logger.debug(message); + } + } + + public void logOptions(TransformRequest request) + { + RepositoryClientData repositoryClientData = new RepositoryClientData(request.getClientData()); + if (isEnabled(repositoryClientData)) + { + Map options = request.getTransformRequestOptions(); + if (options != null && !options.isEmpty()) + { + String reference = TransformStack.getReference(request.getInternalContext()); + for (Map.Entry option : options.entrySet()) + { + String key = option.getKey(); + if (!TIMEOUT.equals(key)) + { + String value = option.getValue(); + String message = getOptionAndValue(reference, key, value); + logger.debug(message); + if (isDebugToBeReturned(repositoryClientData)) + { + repositoryClientData.appendDebug(message); + } + } + } + request.setClientData(repositoryClientData.toString()); + } + } + } + + + public void logOptions(String reference, Map options) + { + if (logger.isDebugEnabled() && options != null && !options.isEmpty()) + { + for (Map.Entry option : options.entrySet()) + { + String key = option.getKey(); + if (!TIMEOUT.equals(key)) + { + String value = option.getValue(); + String message = getOptionAndValue(reference, key, value); + logger.debug(message); + } + } + } + } + + String getOptionAndValue(String reference, String key, String value) + { + // Truncate the value if it is long or needs to be protected, like Direct Access Urls + int len = value.length(); + if (len > MAX_OPTION_VALUE) + { + value = value.substring(0, MAX_OPTION_VALUE-MAX_OPTION_DOTS.length()-MAX_OPTION_END_CHARS) + + MAX_OPTION_DOTS +value.substring(len-MAX_OPTION_END_CHARS); + } + return getPaddedReference(reference) + + " " + key + "=\"" + value.replaceAll("\"", "\\\"") + "\""; + } + + public void logFailure(TransformReply reply) + { + RepositoryClientData repositoryClientData = new RepositoryClientData(reply.getClientData()); + if (isEnabled(repositoryClientData)) + { + String reference = TransformStack.getReference(reply.getInternalContext()); + String message = getPaddedReference(reference) + reply.getErrorDetails(); + logger.debug(message); + if (isDebugToBeReturned(repositoryClientData)) + { + repositoryClientData.appendDebug(message); + reply.setClientData(repositoryClientData.toString()); + } + } + } + + public void logFailure(String reference, String message) + { + if (logger.isDebugEnabled()) + { + logger.debug(getPaddedReference(reference) + message); + } + } + + // T-Engines call this method, as the T-Router will appended the same debug messages + public TransformerDebug setIsTEngine(boolean isTEngine) + { + this.isTEngine = isTEngine; + return this; + } + + private boolean isEnabled(RepositoryClientData repositoryClientData) + { + return logger.isDebugEnabled() || isDebugToBeReturned(repositoryClientData); + } + + private boolean isDebugToBeReturned(RepositoryClientData repositoryClientData) + { + return !isTEngine() && repositoryClientData.isDebugRequested(); + } + + private boolean isTEngine() + { + return isTEngine; + } + + private boolean isTopLevel(String reference) + { + return !reference.contains("."); + } + + private String getPaddedReference(String reference) + { + return reference + spaces(REFERENCE_SIZE + 3 - reference.length()); // 3 for "a) " ordered list + } + + private String getMimetypeExt(String mimetype) + { + return padExt(ExtensionService.getExtensionForMimetype(mimetype), mimetype); + } + + public String getTargetMimetypeExt(String targetMimetype, String sourceMimetype) + { + return padExt(ExtensionService.getExtensionForTargetMimetype(targetMimetype, sourceMimetype), targetMimetype); + } + + private String padExt(String mimetypeExt, String mimetype) + { + StringBuilder sb = new StringBuilder(""); + if (mimetypeExt == null) + { + sb.append(mimetype); + } + else + { + sb.append(mimetypeExt); + sb.append(spaces(4 - mimetypeExt.length())); // Pad to normal max ext (4) + } + sb.append(' '); + return sb.toString(); + } + + private String getRenditionName(String renditionName) + { + return !renditionName.isEmpty() + ? "-- "+ replaceWithMetadataRenditionNameIfEmbedOrExtract(renditionName)+" -- " + : ""; + } + + private static String replaceWithMetadataRenditionNameIfEmbedOrExtract(String renditionName) + { + String transformName = getTransformName(renditionName); + return transformName.startsWith(MIMETYPE_METADATA_EXTRACT) + ? "metadataExtract" + : transformName.startsWith(MIMETYPE_METADATA_EMBED) + ? "metadataEmbed" + : renditionName; + } + + private static String getTransformName(String renditionName) + { + return !renditionName.startsWith(TRANSFORM_NAMESPACE) + ? "" + : renditionName.substring(TRANSFORM_NAMESPACE.length()); + } + + private String spaces(int i) + { + StringBuilder sb = new StringBuilder(""); + while (--i >= 0) + { + sb.append(' '); + } + return sb.toString(); + } + + private String ms(long time) + { + return String.format("%,d ms", time); + } + + private String fileSize(long size) + { + if (size < 0) + { + return "unlimited"; + } + if (size == 1) + { + return "1 byte"; + } + final String[] units = new String[] { "bytes", "KB", "MB", "GB", "TB" }; + long divider = 1; + for(int i = 0; i < units.length-1; i++) + { + long nextDivider = divider * 1024; + if (size < nextDivider) + { + return fileSizeFormat(size, divider, units[i]); + } + divider = nextDivider; + } + return fileSizeFormat(size, divider, units[units.length-1]); + } + + private String fileSizeFormat(long size, long divider, String unit) + { + size = size * 10 / divider; + int decimalPoint = (int) size % 10; + + StringBuilder sb = new StringBuilder(); + sb.append(size/10); + if (decimalPoint != 0) + { + sb.append("."); + sb.append(decimalPoint); + } + sb.append(' '); + sb.append(unit); + + return sb.toString(); + } +} diff --git a/alfresco-transform-model/src/test/java/org/alfresco/transform/client/model/TransformRequestValidatorTest.java b/alfresco-transform-model/src/test/java/org/alfresco/transform/client/model/TransformRequestValidatorTest.java new file mode 100644 index 00000000..ca90822b --- /dev/null +++ b/alfresco-transform-model/src/test/java/org/alfresco/transform/client/model/TransformRequestValidatorTest.java @@ -0,0 +1,198 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.model; + +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_IMAGE_PNG; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_PDF; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.UUID; + +import org.junit.Test; +import org.springframework.validation.DirectFieldBindingResult; +import org.springframework.validation.Errors; + +/** + * TransformRequestValidatorTest + *

    + * Unit test that checks the Transform request validation. + */ +public class TransformRequestValidatorTest +{ + private TransformRequestValidator validator = new TransformRequestValidator(); + + @Test + public void testSupports() + { + assertTrue(validator.supports(TransformRequest.class)); + } + + @Test + public void testNullRequest() + { + Errors errors = new DirectFieldBindingResult(null, "request"); + validator.validate(null, errors); + + assertEquals(1, errors.getAllErrors().size()); + assertEquals("request cannot be null", + errors.getAllErrors().iterator().next().getDefaultMessage()); + } + + @Test + public void testMissingId() + { + TransformRequest request = new TransformRequest(); + Errors errors = new DirectFieldBindingResult(request, "request"); + + validator.validate(request, errors); + + assertFalse(errors.getAllErrors().isEmpty()); + assertEquals("requestId cannot be null or empty", + errors.getAllErrors().iterator().next().getDefaultMessage()); + } + + @Test + public void testMissingSourceSize() + { + TransformRequest request = new TransformRequest(); + request.setRequestId(UUID.randomUUID().toString()); + request.setSourceReference(UUID.randomUUID().toString()); + Errors errors = new DirectFieldBindingResult(request, "request"); + + validator.validate(request, errors); + + assertFalse(errors.getAllErrors().isEmpty()); + assertEquals("sourceSize cannot be null or have its value smaller than 0", + errors.getAllErrors().iterator().next().getDefaultMessage()); + } + + @Test + public void testMissingSourceMediaType() + { + TransformRequest request = new TransformRequest(); + request.setRequestId(UUID.randomUUID().toString()); + request.setSourceReference(UUID.randomUUID().toString()); + request.setSourceSize(32L); + Errors errors = new DirectFieldBindingResult(request, "request"); + + validator.validate(request, errors); + + assertFalse(errors.getAllErrors().isEmpty()); + assertEquals("sourceMediaType cannot be null or empty", + errors.getAllErrors().iterator().next().getDefaultMessage()); + } + + @Test + public void testMissingTargetMediaType() + { + TransformRequest request = new TransformRequest(); + request.setRequestId(UUID.randomUUID().toString()); + request.setSourceReference(UUID.randomUUID().toString()); + request.setSourceSize(32L); + request.setSourceMediaType(MIMETYPE_PDF); + Errors errors = new DirectFieldBindingResult(request, "request"); + + validator.validate(request, errors); + + assertFalse(errors.getAllErrors().isEmpty()); + assertEquals("targetMediaType cannot be null or empty", + errors.getAllErrors().iterator().next().getDefaultMessage()); + } + + @Test + public void testMissingTargetExtension() + { + TransformRequest request = new TransformRequest(); + request.setRequestId(UUID.randomUUID().toString()); + request.setSourceReference(UUID.randomUUID().toString()); + request.setSourceSize(32L); + request.setSourceMediaType(MIMETYPE_PDF); + request.setTargetMediaType(MIMETYPE_IMAGE_PNG); + Errors errors = new DirectFieldBindingResult(request, "request"); + + validator.validate(request, errors); + + assertFalse(errors.getAllErrors().isEmpty()); + assertEquals("targetExtension cannot be null or empty", + errors.getAllErrors().iterator().next().getDefaultMessage()); + } + + @Test + public void testMissingClientData() + { + TransformRequest request = new TransformRequest(); + request.setRequestId(UUID.randomUUID().toString()); + request.setSourceReference(UUID.randomUUID().toString()); + request.setSourceSize(32L); + request.setSourceMediaType(MIMETYPE_PDF); + request.setTargetMediaType(MIMETYPE_IMAGE_PNG); + request.setTargetExtension("png"); + Errors errors = new DirectFieldBindingResult(request, "request"); + + validator.validate(request, errors); + + assertFalse(errors.getAllErrors().isEmpty()); + assertEquals("clientData cannot be null or empty", + errors.getAllErrors().iterator().next().getDefaultMessage()); + } + + @Test + public void testMissingSchema() + { + TransformRequest request = new TransformRequest(); + request.setRequestId(UUID.randomUUID().toString()); + request.setSourceReference(UUID.randomUUID().toString()); + request.setSourceSize(32L); + request.setSourceMediaType(MIMETYPE_PDF); + request.setTargetMediaType(MIMETYPE_IMAGE_PNG); + request.setTargetExtension("png"); + request.setClientData("ACS"); + request.setSchema(-1); + Errors errors = new DirectFieldBindingResult(request, "request"); + + validator.validate(request, errors); + + assertFalse(errors.getAllErrors().isEmpty()); + assertEquals("schema cannot be less than 0", + errors.getAllErrors().iterator().next().getDefaultMessage()); + } + + @Test + public void testCompleteTransformRequest() + { + TransformRequest request = new TransformRequest(); + request.setRequestId(UUID.randomUUID().toString()); + request.setSourceReference(UUID.randomUUID().toString()); + request.setSourceSize(32L); + request.setSourceMediaType(MIMETYPE_PDF); + request.setTargetMediaType(MIMETYPE_IMAGE_PNG); + request.setTargetExtension("png"); + request.setClientData("ACS"); + Errors errors = new DirectFieldBindingResult(request, "request"); + + validator.validate(request, errors); + + assertTrue(errors.getAllErrors().isEmpty()); + } +} diff --git a/alfresco-transform-model/src/test/java/org/alfresco/transform/client/model/config/CoreFunctionTest.java b/alfresco-transform-model/src/test/java/org/alfresco/transform/client/model/config/CoreFunctionTest.java new file mode 100644 index 00000000..bcac6e7d --- /dev/null +++ b/alfresco-transform-model/src/test/java/org/alfresco/transform/client/model/config/CoreFunctionTest.java @@ -0,0 +1,50 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.model.config; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class CoreFunctionTest +{ + @Test + void isSupported() + { + assertTrue(CoreFunction.HTTP.isSupported("0.1")); + assertTrue(CoreFunction.HTTP.isSupported("2.5.6")); + assertFalse(CoreFunction.HTTP.isSupported("100000")); + + assertFalse(CoreFunction.ACTIVE_MQ.isSupported("0.1")); + assertTrue(CoreFunction.ACTIVE_MQ.isSupported("2.5")); + + assertFalse(CoreFunction.DIRECT_ACCESS_URL.isSupported(null)); + assertFalse(CoreFunction.DIRECT_ACCESS_URL.isSupported("")); + assertFalse(CoreFunction.DIRECT_ACCESS_URL.isSupported("2.5")); + assertFalse(CoreFunction.DIRECT_ACCESS_URL.isSupported("2.5.6")); + assertTrue(CoreFunction.DIRECT_ACCESS_URL.isSupported("2.5.7-SNAPSHOT")); + assertTrue(CoreFunction.DIRECT_ACCESS_URL.isSupported("2.5.7-A4-SNAPSHOT")); + assertTrue(CoreFunction.DIRECT_ACCESS_URL.isSupported("2.5.7")); + assertTrue(CoreFunction.DIRECT_ACCESS_URL.isSupported("2.6")); + assertTrue(CoreFunction.DIRECT_ACCESS_URL.isSupported("999999")); + } +} \ No newline at end of file diff --git a/alfresco-transform-model/src/test/java/org/alfresco/transform/client/model/config/CoreVersionDecoratorTest.java b/alfresco-transform-model/src/test/java/org/alfresco/transform/client/model/config/CoreVersionDecoratorTest.java new file mode 100644 index 00000000..2794a3b7 --- /dev/null +++ b/alfresco-transform-model/src/test/java/org/alfresco/transform/client/model/config/CoreVersionDecoratorTest.java @@ -0,0 +1,216 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.model.config; + +import com.google.common.collect.ImmutableList; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.alfresco.transform.client.model.config.CoreVersionDecorator.CONFIG_VERSION_INCLUDES_CORE_VERSION; +import static org.alfresco.transform.client.model.config.CoreVersionDecorator.setCoreVersionOnMultiStepTransformers; +import static org.alfresco.transform.client.model.config.CoreVersionDecorator.setCoreVersionOnSingleStepTransformers; +import static org.alfresco.transform.client.model.config.CoreVersionDecorator.setOrClearCoreVersion; +import static org.alfresco.transform.client.util.RequestParamMap.CONFIG_VERSION_DEFAULT; +import static org.alfresco.transform.client.util.RequestParamMap.DIRECT_ACCESS_URL; +import static org.junit.jupiter.api.Assertions.*; + +class CoreVersionDecoratorTest +{ + private static final int CONFIG_VERSION_ORIGINAL = Integer.valueOf(CONFIG_VERSION_DEFAULT); + + private static final String SOME_NAME = "optionName"; + + public static final Set SOME_OPTIONS = Set.of(new TransformOptionValue(false, "someOption")); + private static final Set DIRECT_ACCESS_URL_OPTION = Set.of(new TransformOptionValue(false, DIRECT_ACCESS_URL)); + + private final Map> TRANSFORM_OPTIONS_WITHOUT_DIRECT_ACCESS_URL = new HashMap<>(); + private final Map> TRANSFORM_OPTIONS_WITH_DIRECT_ACCESS_URL = new HashMap<>(); + { + TRANSFORM_OPTIONS_WITHOUT_DIRECT_ACCESS_URL.put(SOME_NAME, SOME_OPTIONS); + TRANSFORM_OPTIONS_WITH_DIRECT_ACCESS_URL.put(SOME_NAME, SOME_OPTIONS); + TRANSFORM_OPTIONS_WITH_DIRECT_ACCESS_URL.put(DIRECT_ACCESS_URL, DIRECT_ACCESS_URL_OPTION); + } + + private TransformConfig newTransformConfig(String version1, String version2, String version3, String version4, String version5, + boolean hasDirectAccessUrls, boolean multiStepHaveDirectAccessUrls) + { + HashSet transformOptions1 = new HashSet<>(); + HashSet transformOptions2 = new HashSet<>(transformOptions1); + transformOptions2.add(SOME_NAME); + HashSet transformOptions3 = new HashSet<>(transformOptions1); + + HashSet transformOptions4 = new HashSet<>(transformOptions1); + transformOptions4.addAll(transformOptions2); + HashSet transformOptions5 = new HashSet<>(transformOptions1); + transformOptions5.addAll(transformOptions2); + transformOptions5.addAll(transformOptions3); + + if (hasDirectAccessUrls) + { + transformOptions1.add(DIRECT_ACCESS_URL); + transformOptions2.add(DIRECT_ACCESS_URL); + transformOptions3.add(DIRECT_ACCESS_URL); + } + + if (multiStepHaveDirectAccessUrls) + { + transformOptions4.add(DIRECT_ACCESS_URL); + transformOptions5.add(DIRECT_ACCESS_URL); + } + + return TransformConfig.builder() + .withTransformOptions(hasDirectAccessUrls ? TRANSFORM_OPTIONS_WITH_DIRECT_ACCESS_URL : TRANSFORM_OPTIONS_WITHOUT_DIRECT_ACCESS_URL) + .withTransformers(ImmutableList.of( + Transformer.builder() + .withTransformerName("transformer1") + .withCoreVersion(version1) + .withTransformOptions(transformOptions1) + .build(), + Transformer.builder() + .withTransformerName("transformer2") + .withCoreVersion(version2) + .withTransformOptions(transformOptions2) + .build(), + Transformer.builder() + .withTransformerName("transformer3") + .withCoreVersion(version3) + .withTransformOptions(transformOptions3) + .build(), + Transformer.builder() + .withTransformerName("pipeline4") + .withCoreVersion(version4) + .withTransformerPipeline(List.of( + new TransformStep("transformer1", "mimetype/c"), + new TransformStep("transformer2", null))) + .withTransformOptions(transformOptions4) + .build(), + Transformer.builder() + .withTransformerName("failover5") + .withCoreVersion(version5) + .withTransformerFailover(List.of("transformer1", "transformer2", "transformer3")) + .withTransformOptions(transformOptions5) + .build())) + .build(); + } + + @Test + void setCoreVersionOnSingleStepTransformersTest() + { + TransformConfig transformConfigReadFormTEngineJson = newTransformConfig( + null, null, null, null, null, + false, false); + + setCoreVersionOnSingleStepTransformers(transformConfigReadFormTEngineJson, "2.3.1"); + assertEquals(newTransformConfig("2.3.1", "2.3.1", "2.3.1", null, null, + false, false), transformConfigReadFormTEngineJson); + + // Now with Direct Access URLs + transformConfigReadFormTEngineJson = newTransformConfig( + null, null, null, null, null, + false, false); + + setCoreVersionOnSingleStepTransformers(transformConfigReadFormTEngineJson, "2.5.7"); + assertEquals(newTransformConfig("2.5.7", "2.5.7", "2.5.7", null, null, + true, false), transformConfigReadFormTEngineJson); + } + + @Test + void setCoreVersionOnMultiStepTransformersTest() + { + // All source T-Engines provide a coreVersion and have had the coreVersion added + TransformConfig decoratedSingleStepTransformConfig = newTransformConfig( + "2.1", "2.2", "1.2.3", null, null, + false, false); + + setCoreVersionOnMultiStepTransformers(decoratedSingleStepTransformConfig.getTransformOptions(), + decoratedSingleStepTransformConfig.getTransformers()); + assertEquals(newTransformConfig("2.1", "2.2", "1.2.3", "2.1", "1.2.3", + false, false), decoratedSingleStepTransformConfig); + + // Some source T-Engines are pre coreVersion + decoratedSingleStepTransformConfig = newTransformConfig( + "2.1", null, null, null, null, + false, false); + + setCoreVersionOnMultiStepTransformers(decoratedSingleStepTransformConfig.getTransformOptions(), + decoratedSingleStepTransformConfig.getTransformers()); + assertEquals(newTransformConfig("2.1", null, null, null, null, + false, false), decoratedSingleStepTransformConfig); + + // Now with Direct Access URLs + decoratedSingleStepTransformConfig = newTransformConfig("2.5.7", "2.5.7", "2.5.7", null, null, + true, false); + + setCoreVersionOnMultiStepTransformers(decoratedSingleStepTransformConfig.getTransformOptions(), + decoratedSingleStepTransformConfig.getTransformers()); + assertEquals(newTransformConfig("2.5.7", "2.5.7", "2.5.7", "2.5.7", "2.5.7", + true, true), decoratedSingleStepTransformConfig); + } + + @Test + void setOrClearCoreVersionTest() + { + // All source T-Engines provide a coreVersion + TransformConfig transformConfigWithCoreVersion = newTransformConfig( + "2.1", "2.2", "1.2.3", "2.1", "1.2.3", + false, false); + + assertEquals(newTransformConfig(null, null, null, null, null, + false, false), + setOrClearCoreVersion(transformConfigWithCoreVersion, CONFIG_VERSION_ORIGINAL)); + assertEquals(newTransformConfig("2.1", "2.2", "1.2.3", "2.1", "1.2.3", + false, false), + setOrClearCoreVersion(transformConfigWithCoreVersion, CONFIG_VERSION_INCLUDES_CORE_VERSION)); + assertEquals(newTransformConfig("2.1", "2.2", "1.2.3", "2.1", "1.2.3", + false, false), + setOrClearCoreVersion(transformConfigWithCoreVersion, CONFIG_VERSION_INCLUDES_CORE_VERSION+100)); + + // Some source T-Engines are pre coreVersion + TransformConfig transformConfigWithoutCoreVersion = newTransformConfig( + null, null, null, null, null, + false, false); + + assertEquals(newTransformConfig(null, null, null, null, null, + false, false), + setOrClearCoreVersion(transformConfigWithoutCoreVersion, CONFIG_VERSION_ORIGINAL)); + assertEquals(newTransformConfig(null, null, null, null, null, + false, false), + setOrClearCoreVersion(transformConfigWithoutCoreVersion, CONFIG_VERSION_INCLUDES_CORE_VERSION)); + + // Now with Direct Access URLs + transformConfigWithCoreVersion = newTransformConfig( + "2.5.7", "2.5.7", "2.5.7", "2.5.7", "2.5.7", + true, true); + + assertEquals(newTransformConfig(null, null, null, null, null, + false, false), + setOrClearCoreVersion(transformConfigWithCoreVersion, CONFIG_VERSION_ORIGINAL)); + assertEquals(newTransformConfig("2.5.7", "2.5.7", "2.5.7", "2.5.7", "2.5.7", + true, true), + setOrClearCoreVersion(transformConfigWithCoreVersion, CONFIG_VERSION_INCLUDES_CORE_VERSION)); + } +} \ No newline at end of file diff --git a/alfresco-transform-model/src/test/java/org/alfresco/transform/client/registry/CombinedTransformConfigTest.java b/alfresco-transform-model/src/test/java/org/alfresco/transform/client/registry/CombinedTransformConfigTest.java new file mode 100644 index 00000000..818a0e50 --- /dev/null +++ b/alfresco-transform-model/src/test/java/org/alfresco/transform/client/registry/CombinedTransformConfigTest.java @@ -0,0 +1,896 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2021 - 2022 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.registry; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import org.alfresco.transform.client.model.config.SupportedSourceAndTarget; +import org.alfresco.transform.client.model.config.TransformConfig; +import org.alfresco.transform.client.model.config.TransformStep; +import org.alfresco.transform.client.model.config.Transformer; +import org.junit.Test; + +import java.util.List; +import java.util.Set; + +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; +import static java.util.Collections.emptySet; +import static junit.framework.TestCase.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +/** + * Test the CombinedTransformConfig, extended by both T-Router and ACS repository. + * + * @author adavis + */ +public class CombinedTransformConfigTest +{ + private static final Transformer TRANSFORMER1_A2B_A2E = Transformer.builder().withTransformerName("1") + .withSupportedSourceAndTargetList(Set.of( + SupportedSourceAndTarget.builder() + .withSourceMediaType("mimetype/a") + .withTargetMediaType("mimetype/b") + .build(), + SupportedSourceAndTarget.builder() + .withSourceMediaType("mimetype/a") + .withTargetMediaType("mimetype/e") + .build())) + .build(); + + private static final Transformer TRANSFORMER1_A2B = Transformer.builder().withTransformerName("1") + .withSupportedSourceAndTargetList(Set.of( + SupportedSourceAndTarget.builder() + .withSourceMediaType("mimetype/a") + .withTargetMediaType("mimetype/b") + .build())) + .build(); + + private static final Transformer TRANSFORMER1_A2E = Transformer.builder().withTransformerName("1") + .withSupportedSourceAndTargetList(Set.of( + SupportedSourceAndTarget.builder() + .withSourceMediaType("mimetype/a") + .withTargetMediaType("mimetype/e") + .build())) + .build(); + + private static final Transformer TRANSFORMER1_A2D = Transformer.builder().withTransformerName("1") + .withSupportedSourceAndTargetList(Set.of( + SupportedSourceAndTarget.builder() + .withSourceMediaType("mimetype/a") + .withTargetMediaType("mimetype/d") + .build())) + .build(); + + // Not static as pipelines gets modified. + private static final Transformer PIPELINE1_2C3 = Transformer.builder().withTransformerName("1") + .withTransformerPipeline(List.of( + new TransformStep("2", "mimetype/c"), + new TransformStep("3", null))) + .build(); + + // Not static as pipelines gets modified. + private static final Transformer PIPELINE1_2C3_COPY = Transformer.builder().withTransformerName("1") + .withTransformerPipeline(List.of( + new TransformStep("2", "mimetype/c"), + new TransformStep("3", null))) + .build(); + + private static final Transformer TRANSFORMER2_B2C = Transformer.builder().withTransformerName("2") + .withSupportedSourceAndTargetList(Set.of( + SupportedSourceAndTarget.builder() + .withSourceMediaType("mimetype/b") + .withTargetMediaType("mimetype/c") + .build())) + .build(); + + private static final Transformer TRANSFORMER2_A2C = Transformer.builder().withTransformerName("2") + .withSupportedSourceAndTargetList(Set.of( + SupportedSourceAndTarget.builder() + .withSourceMediaType("mimetype/a") + .withTargetMediaType("mimetype/c") + .build())) + .build(); + + private static final Transformer TRANSFORMER2_B2X = Transformer.builder().withTransformerName("2") + .withSupportedSourceAndTargetList(Set.of( + SupportedSourceAndTarget.builder() + .withSourceMediaType("mimetype/b") + .withTargetMediaType("mimetype/X") + .build())) + .build(); + + private static final Transformer TRANSFORMER3_C2D = Transformer.builder().withTransformerName("3") + .withSupportedSourceAndTargetList(Set.of( + SupportedSourceAndTarget.builder() + .withSourceMediaType("mimetype/c") + .withTargetMediaType("mimetype/d") + .build())) + .build(); + + private static final Transformer TRANSFORMER3_C2D_C2E = Transformer.builder().withTransformerName("3") + .withSupportedSourceAndTargetList(Set.of( + SupportedSourceAndTarget.builder() + .withSourceMediaType("mimetype/c") + .withTargetMediaType("mimetype/d") + .build(), + SupportedSourceAndTarget.builder() + .withSourceMediaType("mimetype/a") + .withTargetMediaType("mimetype/e") + .build())) + .build(); + + private static final Transformer TRANSFORMER1 = Transformer.builder().withTransformerName("1").build(); + + private static final Transformer TRANSFORMER2 = Transformer.builder().withTransformerName("2").build(); + + private static final Transformer TRANSFORMER3 = Transformer.builder().withTransformerName("3").build(); + + public static final Transformer TRANSFORMER_WITH_NO_NAME = Transformer.builder().withTransformerName("").build(); + + public static final String PASS_THROUGH_NAME = "PassThrough"; + + private static final Transformer PASS_THROUGH = Transformer.builder().withTransformerName(PASS_THROUGH_NAME).build(); + + private static final Transformer PASS_THROUGH_COPY = Transformer.builder().withTransformerName(PASS_THROUGH_NAME).build(); + + // Not static as pipelines get modified. + private final Transformer PIPELINE4_1B2C3 = Transformer.builder().withTransformerName("4") + .withTransformerPipeline(List.of( + new TransformStep("1", "mimetype/b"), + new TransformStep("2", "mimetype/c"), + new TransformStep("3", null))) + .build(); + + // Not static as failover transforms get modified. + private final Transformer FAILOVER1_23 = Transformer.builder().withTransformerName("1") + .withTransformerFailover(List.of("2", "3")) + .build(); + + // Not static as failover transforms get modified. + private final Transformer FAILOVER4_123 = Transformer.builder().withTransformerName("4") + .withTransformerFailover(List.of("1", "2", "3")) + .build(); + + // Not static as the nested pipeline gets modified. + private final TransformConfig ONE_TRANSFORM = TransformConfig + .builder() + .withTransformers(ImmutableList.of(PIPELINE1_2C3)) + .build(); + + private static final TransformConfig TWO_TRANSFORMS = TransformConfig + .builder() + .withTransformOptions(ImmutableMap.of( + "options/1", emptySet(), + "options/2", emptySet())) + .withTransformers(ImmutableList.of(TRANSFORMER2_B2C, TRANSFORMER3_C2D)) + .build(); + + private static final String READ_FROM_A = "readFromA"; + private static final String READ_FROM_B = "readFromB"; + private static final String ROUTER_CONFIG_HAS_NO_BASE_URL = null; + private static final String BASE_URL_B = "baseUrlB"; + + private final CombinedTransformConfig config = new CombinedTransformConfig() + { + @Override + protected boolean isPassThroughTransformName(String name) + { + return PASS_THROUGH_NAME.equals(name); + } + }; + + private final TestTransformRegistry registry = new TestTransformRegistry(); + + private String expectedWildcardError(String errorReason) + { + return "No supported source and target mimetypes could be added to the transformer \"4\" as " + + errorReason + ". Read from " + READ_FROM_B; + } + + // Assumes a t-engine transformer named "1" (normally TRANSFORMER1_A2B_A2E) is being overridden by an A2D transform. + // The overriding transform should be the last element in the tRouterTransformers (or if empty the last element in + // tEngineTransformers). The override is expected to good unless an error message is provided. + // A check is made at the end that A2D is possible and that A2B is not possible. + private void assertOverride(List tEngineTransformers, + List tRouterTransformers, String expectedError) + { + Transformer expectedTransformer = tRouterTransformers.isEmpty() + ? tEngineTransformers.get(tEngineTransformers.size() - 1) + : tRouterTransformers.get(tRouterTransformers.size() - 1); + + final TransformConfig tEngineTransformConfig = TransformConfig.builder() + .withTransformers(tEngineTransformers) + .build(); + final TransformConfig tRouterTransformConfig = TransformConfig.builder() + .withTransformers(tRouterTransformers) + .build(); + + config.addTransformConfig(tEngineTransformConfig, READ_FROM_B, BASE_URL_B, registry); + config.addTransformConfig(tRouterTransformConfig, READ_FROM_A, ROUTER_CONFIG_HAS_NO_BASE_URL, registry); + config.combineTransformerConfig(registry); + config.registerCombinedTransformers(registry); + + TransformConfig transformConfig = config.buildTransformConfig(); + + if (expectedError == null) + { + assertEquals(0, registry.errorMessages.size()); + int numberOfTEngineTransformers = tEngineTransformers.size(); + assertEquals(numberOfTEngineTransformers, config.buildTransformConfig().getTransformers().size()); + + // Check we are using the overriding transformer from the t-router, or if none the last one in the t-engine + assertEquals(numberOfTEngineTransformers, transformConfig.getTransformers().size()); + Transformer actualTransformer = transformConfig.getTransformers().get(numberOfTEngineTransformers - 1); + assertEquals(expectedTransformer, actualTransformer); + assertSame("It should even == the expected transform", expectedTransformer, actualTransformer); + + // Check the baseUrl is that of the original t-engine that will do the work, if the overriding transform + // is a single step transform. + List pipeline = actualTransformer.getTransformerPipeline(); + List failover = actualTransformer.getTransformerFailover(); + boolean isPipeline = pipeline != null && !pipeline.isEmpty(); + boolean isFailover = failover != null && !failover.isEmpty(); + if (!isPipeline && !isFailover) + { + assertEquals(BASE_URL_B, registry.transformerBaseUrls.get(actualTransformer)); + } + + // Double check by finding the overriding transformer for A2D but not the overridden transformer for A2B in + // the registry. + assertEquals("1", registry.findTransformerName("mimetype/a", -1, + "mimetype/d", emptyMap(), null)); + // If the assumption about the overridden transform being A2D was wrong, the following would pass anyway + assertNull(registry.findTransformerName("mimetype/a", -1, + "mimetype/b", emptyMap(), null)); + } + else + { + assertEquals(1, registry.errorMessages.size()); + assertEquals(expectedError, registry.errorMessages.get(0)); + } + } + + // No specific tests for addTransformConfig or buildTransformConfig as they are used in most + // other test methods which will fail if they are not working. + + @Test + public void testClear() + { + config.addTransformConfig(TWO_TRANSFORMS, READ_FROM_B, BASE_URL_B, registry); + assertEquals(2, config.buildTransformConfig().getTransformers().size()); + assertEquals(2, config.buildTransformConfig().getTransformOptions().size()); + + config.clear(); + + assertEquals(0, config.buildTransformConfig().getTransformers().size()); + assertEquals(0, config.buildTransformConfig().getTransformOptions().size()); + } + + @Test + public void testCombineTransformerConfigNoOp() + { + config.addTransformConfig(TWO_TRANSFORMS, READ_FROM_B, BASE_URL_B, registry); + config.addTransformConfig(ONE_TRANSFORM, READ_FROM_A, ROUTER_CONFIG_HAS_NO_BASE_URL, registry); + assertEquals(3, config.buildTransformConfig().getTransformers().size()); + assertEquals(2, config.buildTransformConfig().getTransformOptions().size()); + + config.combineTransformerConfig(registry); + assertEquals(3, config.buildTransformConfig().getTransformers().size()); + assertEquals(2, config.buildTransformConfig().getTransformOptions().size()); + } + + @Test + public void testTransformersAreRegistered() + { + config.addTransformConfig(TWO_TRANSFORMS, READ_FROM_B, BASE_URL_B, registry); + config.addTransformConfig(ONE_TRANSFORM, READ_FROM_A, ROUTER_CONFIG_HAS_NO_BASE_URL, registry); + assertEquals(3, config.buildTransformConfig().getTransformers().size()); + assertEquals(2, config.buildTransformConfig().getTransformOptions().size()); + + config.registerCombinedTransformers(registry); + + assertEquals(3, registry.registeredCount); + assertEquals(1, registry.readFromACount); + assertEquals(2, registry.baseUrlBCount); + } + + @Test + public void testInvalidBothPipelineAndFailover() + { + final Transformer invalidTransformer = Transformer.builder().withTransformerName("2") + .withTransformerPipeline(List.of( + new TransformStep("1", "mimetype/b"), + new TransformStep("2", "mimetype/c"), + new TransformStep("3", null))) + .withTransformerFailover(List.of("1", "2", "3")) + .build(); + final TransformConfig transformConfig = TransformConfig.builder() + .withTransformers(ImmutableList.of( + TRANSFORMER1_A2B_A2E, + TRANSFORMER2_B2C, + TRANSFORMER3_C2D, + invalidTransformer)) + .build(); + + config.addTransformConfig(transformConfig, READ_FROM_B, BASE_URL_B, registry); + config.combineTransformerConfig(registry); + + String expected = "Transformer \"2\" cannot have pipeline and failover sections. Read from readFromB"; + assertEquals(1, registry.errorMessages.size()); + assertEquals(expected, registry.errorMessages.get(0)); + } + + @Test + public void testInvalidNoName() + { + final TransformConfig transformConfig = TransformConfig.builder() + .withTransformers(ImmutableList.of(TRANSFORMER_WITH_NO_NAME)) + .build(); + + config.addTransformConfig(transformConfig, READ_FROM_B, BASE_URL_B, registry); + config.combineTransformerConfig(registry); + + String expected = "Transformer names may not be null. Read from readFromB"; + assertEquals(1, registry.errorMessages.size()); + assertEquals(expected, registry.errorMessages.get(0)); + } + + @Test + public void testInvalidTransformOptionsUnknown() + { + final Transformer transformer = Transformer.builder().withTransformerName("1") + .withTransformOptions(ImmutableSet.of("unknown")) + .build(); + final TransformConfig transformConfig = TransformConfig.builder() + .withTransformers(ImmutableList.of(transformer)) + .build(); + + config.addTransformConfig(transformConfig, READ_FROM_B, BASE_URL_B, registry); + config.combineTransformerConfig(registry); + + String expected = "Transformer \"1\" references \"unknown\" which do not exist. Read from readFromB"; + assertEquals(1, registry.errorMessages.size()); + assertEquals(expected, registry.errorMessages.get(0)); + } + + @Test + public void testInvalidDuplicateTEngineSingleStepTransform() + { + final Transformer transformer = Transformer.builder().withTransformerName("1") + .build(); + final Transformer identicalTransformer = Transformer.builder().withTransformerName("1") + .build(); + final TransformConfig transformConfig = TransformConfig.builder() + .withTransformers(ImmutableList.of( + transformer, + identicalTransformer)) + .build(); + + config.addTransformConfig(transformConfig, READ_FROM_B, BASE_URL_B, registry); + config.combineTransformerConfig(registry); + + String expected = "Transformer \"1\" must be a unique name. Read from readFromB"; + assertEquals(1, registry.errorMessages.size()); + assertEquals(expected, registry.errorMessages.get(0)); + } + + @Test + public void testOverrideSingleStepWithSingleStepInTEngine() + { + final Transformer transformerWithSameNameButDifferentDefinition = Transformer.builder().withTransformerName("1") + .withTransformOptions(ImmutableSet.of("options/1")) + .build(); + final TransformConfig transformConfig = TransformConfig.builder() + .withTransformers(ImmutableList.of( + TRANSFORMER1, + transformerWithSameNameButDifferentDefinition)) + .withTransformOptions(ImmutableMap.of("options/1", emptySet())) + .build(); + + config.addTransformConfig(transformConfig, READ_FROM_B, BASE_URL_B, registry); + config.combineTransformerConfig(registry); + + // Expected: This is the same error as in testInvalidDuplicateTEngineSingleStepTransform + String expected = "Transformer \"1\" must be a unique name. Read from readFromB"; + assertEquals(1, registry.errorMessages.size()); + assertEquals(expected, registry.errorMessages.get(0)); + } + + @Test + public void testInvalidSingleStepTransformInRouter() + { + final TransformConfig transformConfig = TransformConfig.builder() + .withTransformers(ImmutableList.of(TRANSFORMER1)) + .build(); + + config.addTransformConfig(transformConfig, READ_FROM_A, ROUTER_CONFIG_HAS_NO_BASE_URL, registry); + config.combineTransformerConfig(registry); + + String expected = "Single step transformers (such as \"1\") must be defined in a T-Engine rather than in a " + + "pipeline file, unless they are overriding an existing single step definition. Read from readFromA"; + assertEquals(1, registry.errorMessages.size()); + assertEquals(expected, registry.errorMessages.get(0)); + } + + @Test + public void testOverrideSingleStepWithSingleStepInTRouter() // i.e. t-router redefines a t-engine single step transform + { + assertOverride( + ImmutableList.of(TRANSFORMER1_A2B_A2E), + ImmutableList.of(TRANSFORMER1_A2D), + null); + } + + @Test + public void testOverrideMultipleTimesSingleStepWithSingleStepInTRouter() // need to make sure the url is remembered + { + assertOverride( + ImmutableList.of(TRANSFORMER1_A2B_A2E), + ImmutableList.of(TRANSFORMER1_A2B, TRANSFORMER1_A2E, TRANSFORMER1_A2D), + null); + } + + @Test + public void testOverrideSingleStepWithPipelineInTRouter() + { + assertOverride( + ImmutableList.of(TRANSFORMER1_A2B_A2E, TRANSFORMER2_A2C, TRANSFORMER3_C2D), + ImmutableList.of(PIPELINE1_2C3), + null); + } + + @Test + public void testOverridePipelineWithPipelineInTRouter() + { + assertOverride( + ImmutableList.of(PIPELINE1_2C3, TRANSFORMER2_A2C, TRANSFORMER3_C2D), + ImmutableList.of(PIPELINE1_2C3_COPY), + null); + } + + @Test + public void testInvalidOverridePipelineWithSingleStepInTRouter() + { + assertOverride( + ImmutableList.of(PIPELINE1_2C3, TRANSFORMER2_A2C, TRANSFORMER3_C2D), + ImmutableList.of(TRANSFORMER1_A2D), + "Single step transformers (such as \"1\") may not override a pipeline or failover " + + "transform as there is no T-Engine to perform work. Read from readFromA"); + } + + @Test + public void testInvalidOverrideFailoverWithSingleStepInTRouter() + { + assertOverride( + ImmutableList.of(FAILOVER1_23, TRANSFORMER2_A2C, TRANSFORMER3_C2D), + ImmutableList.of(TRANSFORMER1_A2D), + "Single step transformers (such as \"1\") may not override a pipeline or failover " + + "transform as there is no T-Engine to perform work. Read from readFromA"); + } + + @Test + public void testSinglePassThroughInTRouter() + { + final TransformConfig transformConfig = TransformConfig.builder() + .withTransformers(ImmutableList.of(PASS_THROUGH)) + .build(); + + config.addTransformConfig(transformConfig, READ_FROM_A, ROUTER_CONFIG_HAS_NO_BASE_URL, registry); + config.combineTransformerConfig(registry); + + assertEquals(0, registry.errorMessages.size()); + } + + @Test + public void testMultiplePassThroughInTRouter() + { + final TransformConfig transformConfig = TransformConfig.builder() + .withTransformers(ImmutableList.of(PASS_THROUGH, PASS_THROUGH_COPY)) + .build(); + + config.addTransformConfig(transformConfig, READ_FROM_A, ROUTER_CONFIG_HAS_NO_BASE_URL, registry); + config.combineTransformerConfig(registry); + + String expected = "Pipeline files should not use \"PassThrough\" as a transform name. Read from readFromA"; + assertEquals(1, registry.errorMessages.size()); + assertEquals(expected, registry.errorMessages.get(0)); + } + + @Test + public void testPassThroughInTEngine() + { + final TransformConfig transformConfig = TransformConfig.builder() + .withTransformers(ImmutableList.of(PASS_THROUGH)) + .build(); + + config.addTransformConfig(transformConfig, READ_FROM_B, BASE_URL_B, registry); + config.combineTransformerConfig(registry); + + String expected = "T-Engines should not use \"PassThrough\" as a transform name. Read from readFromB"; + assertEquals(1, registry.errorMessages.size()); + assertEquals(expected, registry.errorMessages.get(0)); + } + + @Test + public void testInvalidOverrideSingleStepWithPipelineInTEngine() + { + assertOverride( + ImmutableList.of(TRANSFORMER1_A2B_A2E, TRANSFORMER2_A2C, TRANSFORMER3_C2D, PIPELINE1_2C3), + emptyList(), + "Transformer \"1\" must be a unique name. Read from readFromB"); + } + + @Test + public void testInvalidOverridePipelineWithSingleStepInTEngine() + { + assertOverride( + ImmutableList.of(TRANSFORMER2_A2C, TRANSFORMER3_C2D, PIPELINE1_2C3, TRANSFORMER1_A2B_A2E), + emptyList(), + "Transformer \"1\" must be a unique name. Read from readFromB"); + } + + @Test + public void testInvalidIndexToRemoveBeforeCurrent() + { + // indexToRemove is is before the current i value, so we are removing an overridden transform + // Code throws an IllegalArgumentException and i is simply decremented to ignore the current value + final TransformConfig tEngineTransformConfig = TransformConfig.builder() + .withTransformers(ImmutableList.of( + TRANSFORMER1_A2B_A2E, + TRANSFORMER2_B2C, + TRANSFORMER3_C2D)) + .build(); + final TransformConfig tRouterTransformConfig = TransformConfig.builder() + .withTransformers(ImmutableList.of( + TRANSFORMER1_A2D)) // this one triggers the removal of 0 + .build(); + + config.addTransformConfig(tEngineTransformConfig, READ_FROM_B, BASE_URL_B, registry); + config.addTransformConfig(tRouterTransformConfig, READ_FROM_A, ROUTER_CONFIG_HAS_NO_BASE_URL, registry); + config.combineTransformerConfig(registry); + + assertEquals(3, config.buildTransformConfig().getTransformers().size()); + } + + @Test + public void testInvalidIndexToRemoveIsCurrent() + { + // Code throws an IllegalArgumentException and i is simply decremented to ignore the current value + final TransformConfig transformConfig = TransformConfig.builder() + .withTransformers(ImmutableList.of( + TRANSFORMER1_A2D, + TRANSFORMER2_B2C, + TRANSFORMER_WITH_NO_NAME, // discarded + TRANSFORMER3_C2D)) + .build(); + + config.addTransformConfig(transformConfig, READ_FROM_B, BASE_URL_B, registry); + config.combineTransformerConfig(registry); + + assertEquals(3, config.buildTransformConfig().getTransformers().size()); + } + + @Test + public void testSortSoNoForwardRefs() + { + final TransformConfig transformConfig = TransformConfig.builder() + .withTransformers(ImmutableList.of( + PIPELINE1_2C3, + TRANSFORMER2_B2C, + TRANSFORMER3_C2D)) + .build(); + + config.addTransformConfig(transformConfig, READ_FROM_B, BASE_URL_B, registry); + // Check order includes forward references after setup + List transformers = config.buildTransformConfig().getTransformers(); + assertEquals(3, transformers.size()); + assertEquals("1", transformers.get(0).getTransformerName()); + assertEquals("2", transformers.get(1).getTransformerName()); + assertEquals("3", transformers.get(2).getTransformerName()); + + config.combineTransformerConfig(registry); + + // Check order changed so there are no forward references after combined + transformers = config.buildTransformConfig().getTransformers(); + assertEquals(3, transformers.size()); + assertEquals("2", transformers.get(0).getTransformerName()); + assertEquals("3", transformers.get(1).getTransformerName()); + assertEquals("1", transformers.get(2).getTransformerName()); + + assertEquals(0, registry.warnMessages.size()); + assertEquals(0, registry.errorMessages.size()); + } + + @Test + public void testInvalidTransformStepNullIntermediateMimetype() + { + final Transformer pipeline = Transformer.builder().withTransformerName("5") + .withTransformerPipeline(List.of( + new TransformStep("1", "mimetype/b"), + new TransformStep("2", null), // should not be null + new TransformStep("3", null))) + .build(); + final TransformConfig transformConfig = TransformConfig.builder() + .withTransformers(ImmutableList.of( + TRANSFORMER1_A2B, + TRANSFORMER2_B2C, + TRANSFORMER3_C2D, + pipeline)) + .build(); + + config.addTransformConfig(transformConfig, READ_FROM_B, BASE_URL_B, registry); + config.combineTransformerConfig(registry); + + String expected = "No supported source and target mimetypes could be added to the transformer \"5\" as " + + "intermediate steps should have a target mimetype. Read from readFromB"; + assertEquals(1, registry.errorMessages.size()); + assertEquals(expected, registry.errorMessages.get(0)); + } + + @Test + public void testInvalidTransformStepNonNullFinalMimetype() + { + final Transformer pipeline = Transformer.builder().withTransformerName("5") + .withTransformerPipeline(List.of( + new TransformStep("1", "mimetype/b"), + new TransformStep("2", "mimetype/c"), + new TransformStep("3", "mimetype/d"))) // the last step's mimetype should be null + .build(); + final TransformConfig transformConfig = TransformConfig.builder() + .withTransformers(ImmutableList.of( + TRANSFORMER1_A2B, + TRANSFORMER2_B2C, + TRANSFORMER3_C2D, + pipeline)) + .build(); + + config.addTransformConfig(transformConfig, READ_FROM_B, BASE_URL_B, registry); + config.combineTransformerConfig(registry); + + String expected = "No supported source and target mimetypes could be added to the transformer \"5\" as " + + "the final step should not have a target mimetype. Read from readFromB"; + assertEquals(1, registry.errorMessages.size()); + assertEquals(expected, registry.errorMessages.get(0)); + } + + @Test + public void testIgnorePipelineWithMissingStep() + { + final Transformer pipeline = Transformer.builder().withTransformerName("1") + .withTransformerPipeline(List.of( + new TransformStep("2", "mimetype/c"), + new TransformStep("3", "mimetype/d"), + new TransformStep("4", null))) + .build(); + final TransformConfig transformConfig = TransformConfig.builder() + .withTransformers(ImmutableList.of( + TRANSFORMER2_B2C, + TRANSFORMER3_C2D, + pipeline)) + .build(); + + config.addTransformConfig(transformConfig, READ_FROM_B, BASE_URL_B, registry); + config.combineTransformerConfig(registry); + + String expected = "Transformer \"1\" ignored as step transforms (\"4\") do not exist. Read from readFromB"; + assertEquals(1, registry.warnMessages.size()); + assertEquals(expected, registry.warnMessages.get(0)); + } + + @Test + public void testIgnorePipelineWithMissingSteps() + { + final Transformer pipeline = Transformer.builder().withTransformerName("1") + .withTransformerPipeline(List.of( + new TransformStep("3", "mimetype/d"), + new TransformStep("4", "mimetype/e"), + new TransformStep("5", "mimetype/f"), + new TransformStep("2", "mimetype/c"), + new TransformStep("6", null))) + .build(); + final TransformConfig transformConfig = TransformConfig.builder() + .withTransformers(ImmutableList.of( + TRANSFORMER2_B2C, + TRANSFORMER3_C2D, + pipeline)) + .build(); + + config.addTransformConfig(transformConfig, READ_FROM_B, BASE_URL_B, registry); + config.combineTransformerConfig(registry); + + String expected = "Transformer \"1\" ignored as step transforms (\"4\", \"5\", \"6\") do not exist. Read from readFromB"; + assertEquals(1, registry.warnMessages.size()); + assertEquals(expected, registry.warnMessages.get(0)); + } + + @Test + public void testInvalidCircularTransformStep() + { + final Transformer pipeline1 = Transformer.builder().withTransformerName("1") + .withTransformerPipeline(List.of( + new TransformStep("2", "mimetype/c"), + new TransformStep("4", null))) + .build(); + final Transformer pipeline4 = Transformer.builder().withTransformerName("4") + .withTransformerPipeline(List.of( + new TransformStep("3", "mimetype/d"), + new TransformStep("5", "mimetype/f"), + new TransformStep("1", null))) + .build(); + final TransformConfig transformConfig = TransformConfig.builder() + .withTransformers(ImmutableList.of( + TRANSFORMER2_B2C, + TRANSFORMER3_C2D, + pipeline1, + pipeline4)) + .build(); + + config.addTransformConfig(transformConfig, READ_FROM_B, BASE_URL_B, registry); + config.combineTransformerConfig(registry); + + assertEquals(2, registry.warnMessages.size()); + assertEquals("Transformer \"1\" ignored as step transforms (\"4\") do not exist. Read from readFromB", + registry.warnMessages.get(0)); + assertEquals("Transformer \"4\" ignored as step transforms (\"1\", \"5\") do not exist. Read from readFromB", + registry.warnMessages.get(1)); + } + + @Test + public void testWildcardNoOp() + { + final TransformConfig transformConfig = TransformConfig.builder() + .withTransformers(ImmutableList.of( + TRANSFORMER1_A2B_A2E, + TRANSFORMER2_B2C, + TRANSFORMER3_C2D)) + .build(); + + config.addTransformConfig(transformConfig, READ_FROM_B, BASE_URL_B, registry); + config.combineTransformerConfig(registry); + config.registerCombinedTransformers(registry); + + assertEquals(0, registry.errorMessages.size()); + assertEquals(3, config.buildTransformConfig().getTransformers().size()); + assertEquals("1", registry.findTransformerName("mimetype/a", -1, + "mimetype/b", emptyMap(), null)); + assertEquals("2", registry.findTransformerName("mimetype/b", -1, + "mimetype/c", emptyMap(), null)); + assertEquals("3", registry.findTransformerName("mimetype/c", -1, + "mimetype/d", emptyMap(), null)); + } + + // It is not possible to test for expectedWildcardError("one of the step transformers is missing") as + // the sortTransformers() method will have already issued another error. + + @Test + public void testWildcardPipeline() + { + final TransformConfig transformConfig = TransformConfig.builder() + .withTransformers(ImmutableList.of( + TRANSFORMER1_A2B_A2E, + TRANSFORMER2_B2C, + TRANSFORMER3_C2D, + PIPELINE4_1B2C3)) + .build(); + + config.addTransformConfig(transformConfig, READ_FROM_B, BASE_URL_B, registry); + config.combineTransformerConfig(registry); + config.registerCombinedTransformers(registry); + + assertEquals(0, registry.errorMessages.size()); + assertEquals(4, config.buildTransformConfig().getTransformers().size()); + assertEquals("4", registry.findTransformerName("mimetype/a", -1, + "mimetype/d", emptyMap(), null)); + assertEquals("1", registry.findTransformerName("mimetype/a", -1, + "mimetype/b", emptyMap(), null)); + } + + @Test + public void testWildcardPipelineExcludeFirstTransformsSupportedFromCartesianProduct() // Exclude more complex path + { + final TransformConfig transformConfig = TransformConfig.builder() + .withTransformers(ImmutableList.of( + TRANSFORMER1_A2B_A2E, + TRANSFORMER2_B2C, + TRANSFORMER3_C2D_C2E, + PIPELINE4_1B2C3)) + .build(); + + config.addTransformConfig(transformConfig, READ_FROM_B, BASE_URL_B, registry); + config.combineTransformerConfig(registry); + config.registerCombinedTransformers(registry); + + assertEquals(0, registry.errorMessages.size()); + assertEquals(4, config.buildTransformConfig().getTransformers().size()); + assertEquals("4", registry.findTransformerName("mimetype/a", -1, + "mimetype/d", emptyMap(), null)); + + // The pipeline could do A2D and A2E, but as A2E is also supported by the first transformer it is not included. + assertEquals("1", registry.findTransformerName("mimetype/a", -1, + "mimetype/e", emptyMap(), null)); + } + + @Test + public void testPipelineUnsupportedIntermediate() + { + final TransformConfig transformConfig = TransformConfig.builder() + .withTransformers(ImmutableList.of( + TRANSFORMER1_A2B_A2E, + TRANSFORMER2_B2X, + TRANSFORMER3_C2D, + PIPELINE4_1B2C3)) + .build(); + + config.addTransformConfig(transformConfig, READ_FROM_B, BASE_URL_B, registry); + config.combineTransformerConfig(registry); + + String expected = expectedWildcardError("the step transformer \"2\" does not support \"mimetype/b\" to \"mimetype/c\""); + assertEquals(1, registry.errorMessages.size()); + assertEquals(expected, registry.errorMessages.get(0)); + + // 4: the pipeline is not removed, but will not be used as it has no supported transforms. + assertEquals(4, config.buildTransformConfig().getTransformers().size()); + } + + @Test + public void testWildcardFailover() + { + final TransformConfig transformConfig = TransformConfig.builder() + .withTransformers(ImmutableList.of( + TRANSFORMER1_A2B_A2E, + TRANSFORMER2_B2C, + TRANSFORMER3_C2D, + FAILOVER4_123)) + .build(); + + config.addTransformConfig(transformConfig, READ_FROM_B, BASE_URL_B, registry); + config.combineTransformerConfig(registry); + config.registerCombinedTransformers(registry); + + assertEquals(0, registry.errorMessages.size()); + assertEquals(4, config.buildTransformConfig().getTransformers().size()); + assertEquals("4", registry.findTransformerName("mimetype/a", -1, + "mimetype/b", emptyMap(), null)); + } + + @Test + public void testWildcardFailoverNoneSupported() + { + final TransformConfig transformConfig = TransformConfig.builder() + .withTransformers(ImmutableList.of( + TRANSFORMER1, + TRANSFORMER2, + TRANSFORMER3, + FAILOVER4_123)) + .build(); + + config.addTransformConfig(transformConfig, READ_FROM_B, BASE_URL_B, registry); + config.combineTransformerConfig(registry); + + String expected = expectedWildcardError("the step transforms don't support any"); + assertEquals(1, registry.errorMessages.size()); + assertEquals(expected, registry.errorMessages.get(0)); + } +} diff --git a/alfresco-transform-model/src/test/java/org/alfresco/transform/client/registry/OverrideTransformConfigTests.java b/alfresco-transform-model/src/test/java/org/alfresco/transform/client/registry/OverrideTransformConfigTests.java new file mode 100644 index 00000000..91216546 --- /dev/null +++ b/alfresco-transform-model/src/test/java/org/alfresco/transform/client/registry/OverrideTransformConfigTests.java @@ -0,0 +1,460 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2021 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.registry; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import org.alfresco.transform.client.model.config.AddSupported; +import org.alfresco.transform.client.model.config.SupportedDefaults; +import org.alfresco.transform.client.model.config.OverrideSupported; +import org.alfresco.transform.client.model.config.RemoveSupported; +import org.alfresco.transform.client.model.config.SupportedSourceAndTarget; +import org.alfresco.transform.client.model.config.TransformConfig; +import org.alfresco.transform.client.model.config.Transformer; +import org.junit.Test; + +import java.util.HashSet; +import java.util.Set; + +import static junit.framework.TestCase.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Tests the json elements: {@code removeTransformers}, {@code addSupported}, {@code removeSupported}, + * {@code overrideSupported} and {@code supportedDefaults}. + */ +public class OverrideTransformConfigTests +{ + private static final String READ_FROM_A = "readFromA"; + private static final String READ_FROM_B = "readFromB"; + private static final String BASE_URL_A = "baseUrlA"; + private static final String BASE_URL_B = "baseUrlB"; + + private final SupportedSourceAndTarget supported_A2B = SupportedSourceAndTarget.builder() + .withSourceMediaType("mimetype/a") + .withTargetMediaType("mimetype/b") + .build(); + + private final SupportedSourceAndTarget supported_A2B__40 = SupportedSourceAndTarget.builder() + .withSourceMediaType("mimetype/a") + .withTargetMediaType("mimetype/b") + .withPriority(40) + .build(); + + private final SupportedSourceAndTarget supported_C2D = SupportedSourceAndTarget.builder() + .withSourceMediaType("mimetype/c") + .withTargetMediaType("mimetype/d") + .build(); + + private final SupportedSourceAndTarget supported_A2D_1234_44 = SupportedSourceAndTarget.builder() + .withSourceMediaType("mimetype/a") + .withTargetMediaType("mimetype/d") + .withMaxSourceSizeBytes(1234L) + .withPriority(44) + .build(); + + private final SupportedSourceAndTarget supported_X2Y_100_23 = SupportedSourceAndTarget.builder() + .withSourceMediaType("mimetype/x") + .withTargetMediaType("mimetype/y") + .withMaxSourceSizeBytes(100L) + .withPriority(23) + .build(); + + private final SupportedSourceAndTarget supported_X2Y_200 = SupportedSourceAndTarget.builder() + .withSourceMediaType("mimetype/x") + .withTargetMediaType("mimetype/y") + .withMaxSourceSizeBytes(200L) + .build(); + + private final TransformConfig transformConfig_A2B_X2Y_100_23 = TransformConfig.builder() + .withTransformers(ImmutableList.of( + Transformer.builder().withTransformerName("1") + .withSupportedSourceAndTargetList(new HashSet<>(Set.of( + supported_A2B, + supported_X2Y_100_23))) + .build())) + .build(); + + private final CombinedTransformConfig config = new CombinedTransformConfig(); + + private final TestTransformRegistry registry = new TestTransformRegistry(); + + @Test + public void testRemoveTransformers() + { + final Transformer transformer1 = Transformer.builder().withTransformerName("1").build(); + final Transformer transformer2 = Transformer.builder().withTransformerName("2").build(); + final Transformer transformer3 = Transformer.builder().withTransformerName("3").build(); + final Transformer transformer4 = Transformer.builder().withTransformerName("4").build(); + + final TransformConfig firstConfig = TransformConfig.builder() + .withTransformers(ImmutableList.of( + transformer1, + transformer2, + transformer3, + transformer4)) + .build(); + final TransformConfig secondConfig = TransformConfig.builder() + .withTransformers(ImmutableList.of( + transformer2)) // Puts transform 2 back again + .withRemoveTransformers(ImmutableSet.of("2", "7", "3", "2", "5")) + .build(); + + config.addTransformConfig(firstConfig, READ_FROM_A, BASE_URL_A, registry); + TransformConfig resultConfig = config.buildTransformConfig(); + assertEquals(4, resultConfig.getTransformers().size()); + + config.addTransformConfig(secondConfig, READ_FROM_B, BASE_URL_B, registry); + resultConfig = config.buildTransformConfig(); + assertEquals(3, resultConfig.getTransformers().size()); + + String expected = "Unable to process \"removeTransformers\": [\"7\", \"5\"]. Read from readFromB"; + assertEquals(1, registry.warnMessages.size()); + assertEquals(expected, registry.warnMessages.get(0)); + } + + @Test + public void testSupportedDefaultsSet() + { + SupportedDefaults default_1A_100 = SupportedDefaults.builder() + .withTransformerName("1") + .withSourceMediaType("mimetype/a") + .withMaxSourceSizeBytes(100L) + .build(); + + SupportedDefaults default_1A_200 = SupportedDefaults.builder() + .withTransformerName("1") + .withSourceMediaType("mimetype/a") + .withMaxSourceSizeBytes(200L) + .build(); + + SupportedDefaults default_2A__45 = SupportedDefaults.builder() + .withTransformerName("2") + .withSourceMediaType("mimetype/a") + .withPriority(45) + .build(); + + SupportedDefaults default_3_400 = SupportedDefaults.builder() + .withTransformerName("3") + .withMaxSourceSizeBytes(400L) + .build(); + + SupportedDefaults default_B_400 = SupportedDefaults.builder() + .withSourceMediaType("mimetype/b") + .withMaxSourceSizeBytes(400L) + .build(); + + SupportedDefaults default_B_500 = SupportedDefaults.builder() + .withSourceMediaType("mimetype/b") + .withMaxSourceSizeBytes(500L) + .build(); + + SupportedDefaults default__600 = SupportedDefaults.builder() + .withMaxSourceSizeBytes(600L) + .build(); + + SupportedDefaults default___45 = SupportedDefaults.builder() + .withPriority(45) + .build(); + + SupportedDefaults default___50 = SupportedDefaults.builder() + .withPriority(50) + .build(); + + SupportedDefaults default__500_50 = SupportedDefaults.builder() + .withMaxSourceSizeBytes(500L) + .withPriority(50) + .build(); + + SupportedDefaults default__600_45 = SupportedDefaults.builder() + .withMaxSourceSizeBytes(600L) + .withPriority(45) + .build(); + + final TransformConfig firstConfig = TransformConfig.builder() + .build(); + + final TransformConfig secondConfig = TransformConfig.builder() + .withSupportedDefaults(ImmutableSet.of( + default_1A_100)) // 0: transformer and source media type default + .build(); + + final TransformConfig thirdConfig = TransformConfig.builder() + .withSupportedDefaults(ImmutableSet.of( + default_1A_200, // 0: transformer and source media type default + default_2A__45, // 0: transformer and source media type default + default_3_400, // 1: transformer default + default_B_400, // 2: source media type default + default_B_500, // 2: source media type default - overrides the previous value 400 defined in the same config + default__500_50, // 3: system wide default - totally overridden by the next lines. + default__600, // 3: system wide default + default___50, // 3: system wide default (combined with the other system default) + default___45)) // 3: system wide default - overrides the value 45 defined in the same config + .build(); + + final TransformConfig fourthConfig = TransformConfig.builder() + .withSupportedDefaults(ImmutableSet.of( + SupportedDefaults.builder() // 3: system wide default + .withMaxSourceSizeBytes(-1L) + .withPriority(45) + .build())) + .build(); + + final TransformConfig fifthConfig = TransformConfig.builder() + .withSupportedDefaults(ImmutableSet.of( + SupportedDefaults.builder() // 3: system wide default (reset to the default, so removed) + .withPriority(50) + .build(), + SupportedDefaults.builder() // Invalid as neither priority nor maxSourceSizeBytes are set + .withTransformerName("9") + .withSourceMediaType("mimetype/z") + .build())) + .build(); + + + config.addTransformConfig(firstConfig, READ_FROM_A, BASE_URL_A, registry); + TransformConfig resultConfig = config.buildTransformConfig(); + assertEquals(0, resultConfig.getSupportedDefaults().size()); + + config.addTransformConfig(secondConfig, READ_FROM_B, BASE_URL_B, registry); + resultConfig = config.buildTransformConfig(); + assertEquals(ImmutableSet.of( + default_1A_100), + resultConfig.getSupportedDefaults()); + + config.addTransformConfig(thirdConfig, READ_FROM_B, BASE_URL_B, registry); + resultConfig = config.buildTransformConfig(); + assertEquals(ImmutableSet.of( + default_1A_200, // overrides default_1A_100 + default_2A__45, + default_3_400, + default_B_500, + default__600_45), // default__600 + default___45 + resultConfig.getSupportedDefaults()); + + config.addTransformConfig(fourthConfig, READ_FROM_B, BASE_URL_B, registry); + resultConfig = config.buildTransformConfig(); + assertEquals(5, resultConfig.getSupportedDefaults().size()); + + config.addTransformConfig(fifthConfig, READ_FROM_A, BASE_URL_A, registry); + resultConfig = config.buildTransformConfig(); + assertEquals(4, resultConfig.getSupportedDefaults().size()); + assertEquals(ImmutableSet.of( + default_1A_200, // overrides default_1A_100 + default_2A__45, + default_3_400, + default_B_500), // default__600_45 removed as the system defaults have been reset to the defaults -1 and 50 + resultConfig.getSupportedDefaults()); + + String expected = "Unable to process \"supportedDefaults\": [" + + "{\"transformerName\": \"9\", \"sourceMediaType\": \"mimetype/z\"}]. Read from readFromA"; + assertEquals(1, registry.warnMessages.size()); + assertEquals(expected, registry.warnMessages.get(0)); + } + + @Test + public void testRemoveSupported() + { + addTransformConfig_A2B_X2Y_100_23(); + + final TransformConfig secondConfig = TransformConfig.builder() + .withRemoveSupported(ImmutableSet.of( + RemoveSupported.builder() + .withTransformerName("1") // c -> d does not exist + .withSourceMediaType("mimetype/c") + .withTargetMediaType("mimetype/d") + .build(), + RemoveSupported.builder() + .withTransformerName("1") + .withSourceMediaType("mimetype/a") + .withTargetMediaType("mimetype/b") + .build(), + RemoveSupported.builder() // transformer does not exist + .withTransformerName("bad") + .withSourceMediaType("mimetype/a") + .withTargetMediaType("mimetype/d") + .build(), + RemoveSupported.builder() // transform name not set + .withSourceMediaType("mimetype/a") + .withTargetMediaType("mimetype/d") + .build(), + RemoveSupported.builder() // source type not set + .withTransformerName("1") + .withTargetMediaType("mimetype/d") + .build(), + RemoveSupported.builder() // target type not set + .withTransformerName("1") + .withSourceMediaType("mimetype/a") + .build())) + .build(); + + String expectedWarnMessage = "Unable to process \"removeSupported\": [" + + "{\"transformerName\": \"bad\", \"sourceMediaType\": \"mimetype/a\", \"targetMediaType\": \"mimetype/d\"}, " + + "{\"transformerName\": \"1\", \"sourceMediaType\": \"mimetype/a\"}, " + + "{\"transformerName\": \"1\", \"sourceMediaType\": \"mimetype/c\", \"targetMediaType\": \"mimetype/d\"}, " + + "{\"sourceMediaType\": \"mimetype/a\", \"targetMediaType\": \"mimetype/d\"}, " + + "{\"transformerName\": \"1\", \"targetMediaType\": \"mimetype/d\"}]. " + + "Read from readFromB"; + ImmutableSet expectedSupported = ImmutableSet.of(supported_X2Y_100_23); + String expectedToString = "[" + + "{\"sourceMediaType\": \"mimetype/x\", \"targetMediaType\": \"mimetype/y\", \"maxSourceSizeBytes\": \"100\", \"priority\": \"23\"}" + + "]"; + + addTransformConfig(secondConfig, expectedWarnMessage, expectedSupported, expectedToString); + } + + @Test + public void testAddSupported() + { + addTransformConfig_A2B_X2Y_100_23(); + + final TransformConfig secondConfig = TransformConfig.builder() + .withAddSupported(ImmutableSet.of( + AddSupported.builder() + .withTransformerName("1") + .withSourceMediaType("mimetype/c") + .withTargetMediaType("mimetype/d") + .build(), + AddSupported.builder() // duplicates original + .withTransformerName("1") + .withSourceMediaType("mimetype/a") + .withTargetMediaType("mimetype/b") + .withPriority(40) + .build(), + AddSupported.builder() + .withTransformerName("1") + .withSourceMediaType("mimetype/a") + .withTargetMediaType("mimetype/d") + .withPriority(44) + .withMaxSourceSizeBytes(1234) + .build(), + AddSupported.builder() // transformer does not exist + .withTransformerName("bad") + .withSourceMediaType("mimetype/a") + .withTargetMediaType("mimetype/d") + .build(), + AddSupported.builder() // transform name not set + .withSourceMediaType("mimetype/a") + .withTargetMediaType("mimetype/d") + .build(), + AddSupported.builder() // source type not set + .withTransformerName("1") + .withTargetMediaType("mimetype/d") + .build(), + AddSupported.builder() // target type not set + .withTransformerName("1") + .withSourceMediaType("mimetype/a") + .build())) + .build(); + + String expectedWarnMessage = "Unable to process \"addSupported\": [" + + "{\"transformerName\": \"1\", \"sourceMediaType\": \"mimetype/a\"}, " + + "{\"transformerName\": \"1\", \"targetMediaType\": \"mimetype/d\"}, " + + "{\"transformerName\": \"1\", \"sourceMediaType\": \"mimetype/a\", \"targetMediaType\": \"mimetype/b\", \"priority\": \"40\"}, " + + "{\"transformerName\": \"bad\", \"sourceMediaType\": \"mimetype/a\", \"targetMediaType\": \"mimetype/d\"}, " + + "{\"sourceMediaType\": \"mimetype/a\", \"targetMediaType\": \"mimetype/d\"}]. " + + "Read from readFromB"; + ImmutableSet expectedSupported = ImmutableSet.of( + supported_A2B, + supported_C2D, + supported_X2Y_100_23, + supported_A2D_1234_44); + String expectedToString = "[" + + "{\"sourceMediaType\": \"mimetype/x\", \"targetMediaType\": \"mimetype/y\", \"maxSourceSizeBytes\": \"100\", \"priority\": \"23\"}, " + + "{\"sourceMediaType\": \"mimetype/a\", \"targetMediaType\": \"mimetype/d\", \"maxSourceSizeBytes\": \"1234\", \"priority\": \"44\"}, " + + "{\"sourceMediaType\": \"mimetype/a\", \"targetMediaType\": \"mimetype/b\"}, " + // priority and size should be missing (i.e. use defaults) + "{\"sourceMediaType\": \"mimetype/c\", \"targetMediaType\": \"mimetype/d\"}" + + "]"; + + addTransformConfig(secondConfig, expectedWarnMessage, expectedSupported, expectedToString); + } + + @Test + public void testOverrideSupported() + { + addTransformConfig_A2B_X2Y_100_23(); + + final TransformConfig secondConfig = TransformConfig.builder() + .withOverrideSupported(ImmutableSet.of( + OverrideSupported.builder() // does not exist + .withTransformerName("1") + .withSourceMediaType("mimetype/c") + .withTargetMediaType("mimetype/d") + .build(), + OverrideSupported.builder() // size default -> 200 and priority default -> 100 + .withTransformerName("1") + .withSourceMediaType("mimetype/a") + .withTargetMediaType("mimetype/b") + .withPriority(40) + .build(), + OverrideSupported.builder() // size 100 -> 200 and change priority to default + .withTransformerName("1") + .withSourceMediaType("mimetype/x") + .withTargetMediaType("mimetype/y") + .withMaxSourceSizeBytes(200) + .build(), + OverrideSupported.builder() // transformer does not exist + .withTransformerName("bad") + .withSourceMediaType("mimetype/a") + .withTargetMediaType("mimetype/d") + .build())) + // OverrideSupported values with missing fields are defaults, so no test values here + .build(); + + String expectedWarnMessage = "Unable to process \"overrideSupported\": [" + + "{\"transformerName\": \"1\", \"sourceMediaType\": \"mimetype/c\", \"targetMediaType\": \"mimetype/d\"}, " + + "{\"transformerName\": \"bad\", \"sourceMediaType\": \"mimetype/a\", \"targetMediaType\": \"mimetype/d\"}]. " + + "Read from readFromB"; + ImmutableSet expectedSupported = ImmutableSet.of( + supported_X2Y_200, + supported_A2B__40); + String expectedToString = "[" + + "{\"sourceMediaType\": \"mimetype/a\", \"targetMediaType\": \"mimetype/b\", \"priority\": \"40\"}, " + + "{\"sourceMediaType\": \"mimetype/x\", \"targetMediaType\": \"mimetype/y\", \"maxSourceSizeBytes\": \"200\"}" + + "]"; + + addTransformConfig(secondConfig, expectedWarnMessage, expectedSupported, expectedToString); + } + + private void addTransformConfig_A2B_X2Y_100_23() + { + config.addTransformConfig(transformConfig_A2B_X2Y_100_23, READ_FROM_A, BASE_URL_A, registry); + TransformConfig resultConfig = config.buildTransformConfig(); + assertEquals(1, resultConfig.getTransformers().size()); + assertEquals(2, resultConfig.getTransformers().get(0).getSupportedSourceAndTargetList().size()); + } + + private void addTransformConfig(TransformConfig secondConfig, String expectedWarnMessage, + Set expectedSupported, String expectedToString) + { + config.addTransformConfig(secondConfig, READ_FROM_B, BASE_URL_B, registry); + TransformConfig resultConfig = config.buildTransformConfig(); + + assertEquals(1, registry.warnMessages.size()); + assertEquals(expectedWarnMessage, registry.warnMessages.get(0)); + + Set supportedSourceAndTargetList = resultConfig.getTransformers().get(0).getSupportedSourceAndTargetList(); + assertTrue(supportedSourceAndTargetList.equals(expectedSupported)); + + assertEquals("toString() difference", expectedToString, supportedSourceAndTargetList.toString()); + } +} diff --git a/alfresco-transform-model/src/test/java/org/alfresco/transform/client/registry/TestTransformRegistry.java b/alfresco-transform-model/src/test/java/org/alfresco/transform/client/registry/TestTransformRegistry.java new file mode 100644 index 00000000..ecc0f675 --- /dev/null +++ b/alfresco-transform-model/src/test/java/org/alfresco/transform/client/registry/TestTransformRegistry.java @@ -0,0 +1,88 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2021 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.registry; + +import org.alfresco.transform.client.model.config.TransformOption; +import org.alfresco.transform.client.model.config.Transformer; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Helper class for testing an {@link AbstractTransformRegistry}. + */ +public class TestTransformRegistry extends AbstractTransformRegistry +{ + private static final String READ_FROM_A = "readFromA"; + private static final String BASE_URL_B = "baseUrlB"; + + private final TransformCache data = new TransformCache(); + + List errorMessages = new ArrayList<>(); + List warnMessages = new ArrayList<>(); + int registeredCount = 0; + int readFromACount = 0; + int baseUrlBCount = 0; + Map transformerBaseUrls = new HashMap<>(); + + @Override + protected void logError(String msg) + { + System.out.println(msg); + errorMessages.add(msg); + } + + @Override + protected void logWarn(String msg) + { + System.out.println(msg); + warnMessages.add(msg); + } + + @Override + public TransformCache getData() + { + return data; + } + + @Override + protected void register(final Transformer transformer, + final Map> transformOptions, final String baseUrl, + final String readFrom) + { + super.register(transformer, transformOptions, baseUrl, readFrom); + + registeredCount++; + if (READ_FROM_A.equals(readFrom)) + { + readFromACount++; + } + if (BASE_URL_B.equals(baseUrl)) + { + baseUrlBCount++; + } + transformerBaseUrls.put(transformer, baseUrl); + } +} diff --git a/alfresco-transform-model/src/test/java/org/alfresco/transform/client/registry/TransformRegistryHelperTest.java b/alfresco-transform-model/src/test/java/org/alfresco/transform/client/registry/TransformRegistryHelperTest.java new file mode 100644 index 00000000..6249760f --- /dev/null +++ b/alfresco-transform-model/src/test/java/org/alfresco/transform/client/registry/TransformRegistryHelperTest.java @@ -0,0 +1,181 @@ +/* + * #%L + * Alfresco Transform Core + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * - + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * - + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * - + * Alfresco 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 Lesser General Public License for more details. + * - + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.transform.client.registry; + +import org.alfresco.transform.exceptions.TransformException; +import org.junit.Test; + +import java.util.List; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptySet; +import static org.alfresco.transform.client.registry.TransformRegistryHelper.retrieveTransformListBySize; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class TransformRegistryHelperTest +{ + @Test + public void testListBySize() + { + // This test was inspired by a failure to pick libreoffice over textToPdf despite the fact libreoffice has a + // higher priority. + SupportedTransform libreoffice = new SupportedTransform("libreoffice", emptySet(), -1, 50); + SupportedTransform textToPdf = new SupportedTransform("textToPdf", emptySet(), 100,55); + + assertOrder(asList(libreoffice, textToPdf), asList(libreoffice)); + assertOrder(asList(textToPdf, libreoffice), asList(libreoffice)); + + // * If multiple transforms with the same priority can support the same size, the one with the highest size + // limit (or no limit) is used. + // * Transforms with a higher priority (lower numerically) are used up to their size limit in preference to + // lower priority transforms. These lower priority transforms will be used above that limit. + // * If there are multiple transforms with the same priority and size limit, the last one defined is used to + // allow extensions to override standard transforms. + // * In each of the above cases, it is possible for supplied transforms not to be returned from + // retrieveTransformListBySize as they will never be used. However this method is currently only used + // by (1) AbstractTransformRegistry.findTransformerName which filters out transformers that cannot support a + // given size and then uses the lowest element and (2) AbstractTransformRegistry.findMaxSize and gets the last + // element without filtering and returns its size limit. So there are opportunities to change the code so that + // it does not actually have to remove transformers that will not be used. + + // Test transforms + SupportedTransform p45 = new SupportedTransform( "p45", emptySet(), -1, 45); + SupportedTransform p50 = new SupportedTransform( "p50", emptySet(), -1, 50); + SupportedTransform p55 = new SupportedTransform( "p55", emptySet(), -1, 55); + SupportedTransform s100p45 = new SupportedTransform("s100p45", emptySet(), 100, 45); + SupportedTransform s100p50 = new SupportedTransform("s100p50", emptySet(), 100, 50); + SupportedTransform s100p55 = new SupportedTransform("s100p55", emptySet(), 100, 55); + SupportedTransform s200p50 = new SupportedTransform("s200p50", emptySet(), 200, 50); + SupportedTransform s200p50b = new SupportedTransform("s200p50b", emptySet(), 200, 50); + SupportedTransform s200p55 = new SupportedTransform("s200p55", emptySet(), 200, 55); + SupportedTransform s300p45 = new SupportedTransform("s300p45", emptySet(), 300, 45); + SupportedTransform s300p50 = new SupportedTransform("s300p50", emptySet(), 300, 50); + SupportedTransform s300p55 = new SupportedTransform("s300p55", emptySet(), 300, 55); + + // Just considers the priority + assertOrder(asList(p50), asList(p50)); + assertOrder(asList(p45, p50), asList(p45)); + assertOrder(asList(p50, p55), asList(p50)); + assertOrder(asList(p50, p45), asList(p45)); + assertOrder(asList(p45, p50, p55), asList(p45)); + assertOrder(asList(p50, p55, p45), asList(p45)); + assertOrder(asList(p50, p45, p55), asList(p45)); + + // Just considers the priority as the size limit is the same + assertOrder(asList(s100p45, s100p50, s100p55), asList(s100p45)); + assertOrder(asList(s100p50, s100p45, s100p55), asList(s100p45)); + + // Just considers size as the priority is the same + assertOrder(asList(s100p50), asList(s100p50)); + assertOrder(asList(s100p50, s200p50), asList(s200p50)); + assertOrder(asList(s200p50, s100p50), asList(s200p50)); + assertOrder(asList(s100p50, s200p50, s300p50), asList(s300p50)); + assertOrder(asList(s200p50, s100p50, s300p50), asList(s300p50)); + assertOrder(asList(s300p50, s200p50, s100p50), asList(s300p50)); + + // Just considers the order in which they were defined as the priority and size limit are the same. + assertOrder(asList(s200p50, s200p50b), asList(s200p50b)); + assertOrder(asList(s200p50b, s200p50), asList(s200p50)); + + // Combinations of priority and a size limit (always set) + assertOrder(asList(s100p45, s100p50, s200p50, s200p55, s300p45, s300p50, s300p55), asList(s300p45)); + assertOrder(asList(s200p50, s300p55, s300p45, s100p45, s100p50, s300p50, s200p55), asList(s300p45)); + assertOrder(asList(s100p45, s200p50, s300p55), asList(s100p45, s200p50, s300p55)); + assertOrder(asList(s200p50, s100p45, s300p55), asList(s100p45, s200p50, s300p55)); + + // Combinations of priority and a size limit or no size limit + assertOrder(asList(p45, s100p50, s200p50, s300p55), asList(p45)); + assertOrder(asList(s100p50, s200p50, s300p55, p45), asList(p45)); + assertOrder(asList(p55, s100p50, s200p50, s300p55), asList(s200p50, p55)); + assertOrder(asList(p50, s100p50, s200p50, s300p55), asList(p50)); + assertOrder(asList(s100p50, s200p50, s300p55, p50), asList(p50)); + } + + private void assertOrder(List transformsInLoadOrder, List expectedList) + { + TransformRegistryHelper helper = new TransformRegistryHelper(); + TransformCache data = new TransformCache(); + transformsInLoadOrder.forEach(t->data.appendTransform("text/plain", "application/pdf", t, + null, null)); + + List supportedTransforms = retrieveTransformListBySize(data, + "text/plain", "application/pdf", null, null); + + // Check the values used. + String transformerName = findTransformerName(supportedTransforms, 1); + long maxSize = findMaxSize(supportedTransforms); + String expectedTransformerName = expectedList.get(0).getName(); + long expectedMaxSourceSizeBytes = findMaxSize(expectedList); + assertEquals(expectedList, supportedTransforms); + assertEquals("Transform name", expectedTransformerName, transformerName); + assertEquals("MaxSize", expectedMaxSourceSizeBytes, maxSize); + + // If the above two pass, we don't really need the following one, but if it is wrong it might indicate + // something is wrong, where the sourceSizeInBytes is not just 1. + assertEquals(expectedList, supportedTransforms); + } + + // Similar to the method in AbstractTransformRegistry + private String findTransformerName(List supportedTransforms, final long sourceSizeInBytes) + { + return supportedTransforms + .stream() + .filter(t -> t.getMaxSourceSizeBytes() == -1 || + t.getMaxSourceSizeBytes() >= sourceSizeInBytes) + .findFirst() + .map(SupportedTransform::getName) + .orElse(null); + } + + // Similar to the method in AbstractTransformRegistry + private long findMaxSize(List supportedTransforms) + { + return supportedTransforms.isEmpty() ? 0 : + supportedTransforms.get(supportedTransforms.size() - 1).getMaxSourceSizeBytes(); + } + + @Test(expected = TransformException.class) + public void buildTransformListSourceMimeTypeNullErrorTest() + { + TransformCache data = new TransformCache(); + + retrieveTransformListBySize(data, null, "application/pdf", null, null); + + fail("No exception raised"); + } + + @Test(expected = TransformException.class) + public void buildTransformListTargetMimeTypeNullErrorTest() + { + TransformCache data = new TransformCache(); + + retrieveTransformListBySize(data, "text/plain", null, null, null); + + fail("No exception raised"); + } +} diff --git a/alfresco-transform-model/src/test/java/org/alfresco/transform/client/registry/TransformRegistryTest.java b/alfresco-transform-model/src/test/java/org/alfresco/transform/client/registry/TransformRegistryTest.java new file mode 100644 index 00000000..7a7e3984 --- /dev/null +++ b/alfresco-transform-model/src/test/java/org/alfresco/transform/client/registry/TransformRegistryTest.java @@ -0,0 +1,622 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2021 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.client.registry; + +import static java.util.Collections.emptyMap; +import static java.util.Collections.emptySet; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.toMap; +import static org.alfresco.transform.client.registry.TransformRegistryHelper.addToPossibleTransformOptions; +import static org.alfresco.transform.client.registry.TransformRegistryHelper.optionsMatch; +import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.alfresco.transform.client.model.config.SupportedSourceAndTarget; +import org.alfresco.transform.client.model.config.TransformConfig; +import org.alfresco.transform.client.model.config.TransformOption; +import org.alfresco.transform.client.model.config.TransformOptionGroup; +import org.alfresco.transform.client.model.config.TransformOptionValue; +import org.alfresco.transform.client.model.config.Transformer; +import org.junit.Before; +import org.junit.Test; + +import com.google.common.collect.ImmutableSet; + +/** + * Test the AbstractTransformRegistry, extended by both T-Engines and ACS repository, which need to + * read JSON config to understand what is supported. + * + * @author adavis + */ +public class TransformRegistryTest +{ + protected static final String GIF = "image/gif"; + protected static final String JPEG = "image/jpeg"; + protected static final String PDF = "application/pdf"; + protected static final String DOC = "application/msword"; + protected static final String XLS = "application/vnd.ms-excel"; + protected static final String PPT = "application/vnd.ms-powerpoint"; + protected static final String MSG = "application/vnd.ms-outlook"; + protected static final String TXT = "text/plain"; + + protected AbstractTransformRegistry registry; + protected Map> mapOfTransformOptions; + + @Before + public void setUp() throws Exception + { + registry = buildTransformServiceRegistryImpl(); + mapOfTransformOptions = new HashMap<>(); + } + + protected AbstractTransformRegistry buildTransformServiceRegistryImpl() throws Exception + { + return new AbstractTransformRegistry() + { + private TransformCache data = new TransformCache(); + + @Override + protected void logError(String msg) + { + System.out.println(msg); + } + + @Override + protected void logWarn(String msg) + { + System.out.println(msg); + } + + @Override + public TransformCache getData() + { + return data; + } + }; + } + + private void assertAddToPossibleOptions(final TransformOptionGroup transformOptionGroup, + final Set actualOptionNames, final Set expectedNameSet, + final Set expectedRequiredSet) + { + final Map possibleTransformOptions = new HashMap<>(); + + addToPossibleTransformOptions(possibleTransformOptions, transformOptionGroup, true, + buildActualOptions(actualOptionNames)); + + assertEquals("The expected options don't match", expectedNameSet, + possibleTransformOptions.keySet()); + + possibleTransformOptions.forEach((name, required) -> { + if (required) + { + assertTrue(name + " should be REQUIRED", expectedRequiredSet.contains(name)); + } + else + { + assertFalse(name + " should be OPTIONAL", expectedRequiredSet.contains(name)); + } + }); + } + + // transformOptionNames are upper case if required. + private void assertIsSupported(final Set actualOptionNames, + final Set transformOptionNames, final String unsupportedMsg) + { + final Map transformOptions = transformOptionNames + .stream() + .collect(toMap(identity(), name -> name.toUpperCase().equals(name))); + + boolean supported = optionsMatch(transformOptions, buildActualOptions(actualOptionNames)); + if (isBlank(unsupportedMsg)) + { + assertTrue("Expected these options to be SUPPORTED", supported); + } + else + { + assertFalse("Expected these options NOT to be supported, because " + unsupportedMsg, + supported); + } + } + + private void assertTransformOptions(Set setOfTransformOptions) throws Exception + { + final Transformer transformer = new Transformer("name", singleton("testOptions"), set( + SupportedSourceAndTarget.builder() + .withSourceMediaType(DOC) + .withTargetMediaType(TXT) + .build(), + SupportedSourceAndTarget.builder() + .withSourceMediaType(XLS) + .withTargetMediaType(TXT) + .withMaxSourceSizeBytes(1024000L) + .build())); + final TransformConfig transformConfig = TransformConfig + .builder() + .withTransformers(singletonList(transformer)) + .withTransformOptions(singletonMap("testOptions", setOfTransformOptions)) + .build(); + + registry = buildTransformServiceRegistryImpl(); + CombinedTransformConfig.combineAndRegister(transformConfig, getClass().getName(), getBaseUrl(transformer), registry); + + assertTrue(registry.isSupported(XLS, 1024, TXT, emptyMap(), null)); + assertTrue(registry.isSupported(XLS, 1024000, TXT, null, null)); + assertFalse(registry.isSupported(XLS, 1024001, TXT, emptyMap(), null)); + assertTrue(registry.isSupported(DOC, 1024001, TXT, null, null)); + } + + protected String getBaseUrl(Transformer transformer) + { + return "xxx"; + } + + private void assertTransformerName(String sourceMimetype, long sourceSizeInBytes, + String targetMimetype, Map actualOptions, String expectedTransformerName, + Transformer... transformers) throws Exception + { + buildAndPopulateRegistry(transformers); + String transformerName = registry.findTransformerName(sourceMimetype, sourceSizeInBytes, + targetMimetype, actualOptions, null); + assertEquals( + sourceMimetype + " to " + targetMimetype + " should have returned " + expectedTransformerName, + expectedTransformerName, transformerName); + } + + private void assertSupported(final Transformer transformer, final String sourceMimetype, + final long sourceSizeInBytes, final String targetMimetype, + final Map actualOptions, final String unsupportedMsg) throws Exception + { + assertSupported(sourceMimetype, sourceSizeInBytes, targetMimetype, actualOptions, + unsupportedMsg, transformer); + } + + private void assertSupported(String sourceMimetype, long sourceSizeInBytes, + String targetMimetype, Map actualOptions, String unsupportedMsg, + Transformer... transformers) throws Exception + { + buildAndPopulateRegistry(transformers); + assertSupported(sourceMimetype, sourceSizeInBytes, targetMimetype, actualOptions, null, + unsupportedMsg); + } + + private void buildAndPopulateRegistry(Transformer[] transformers) throws Exception + { + registry = buildTransformServiceRegistryImpl(); + TransformConfig transformConfig = TransformConfig.builder() + .withTransformers(Arrays.asList(transformers)) + .withTransformOptions(mapOfTransformOptions) + .build(); + CombinedTransformConfig.combineAndRegister(transformConfig, getClass().getName(), "---", registry); + } + + protected void assertSupported(String sourceMimetype, long sourceSizeInBytes, + String targetMimetype, Map actualOptions, String renditionName, + String unsupportedMsg) + { + boolean supported = registry.isSupported(sourceMimetype, sourceSizeInBytes, targetMimetype, + actualOptions, renditionName); + if (unsupportedMsg == null || unsupportedMsg.isEmpty()) + { + assertTrue(sourceMimetype + " to " + targetMimetype + " should be SUPPORTED", + supported); + } + else + { + assertFalse(sourceMimetype + " to " + targetMimetype + " should NOT be supported", + supported); + } + } + + private static Map buildActualOptions(final Set optionNames) + { + return optionNames + .stream() + .collect(toMap(identity(), name -> "value for " + name)); + } + + @Test + public void testOptionalGroups() + { + final TransformOptionGroup transformOptionGroup = + new TransformOptionGroup(true, set( + new TransformOptionValue(false, "1"), + new TransformOptionValue(true, "2"), + new TransformOptionGroup(false, set( + new TransformOptionValue(false, "3.1"), + new TransformOptionValue(false, "3.2"), + new TransformOptionValue(false, "3.3"))), + new TransformOptionGroup(false, set( // OPTIONAL + new TransformOptionValue(false, "4.1"), + new TransformOptionValue(true, "4.2"), + new TransformOptionValue(false, "4.3"))))); + + assertAddToPossibleOptions(transformOptionGroup, emptySet(), + set("1", "2"), set("2")); + assertAddToPossibleOptions(transformOptionGroup, set("1"), + set("1", "2"), set("2")); + assertAddToPossibleOptions(transformOptionGroup, set("2"), + set("1", "2"), set("2")); + assertAddToPossibleOptions(transformOptionGroup, set("2", "3.2"), + set("1", "2", "3.1", "3.2", "3.3"), set("2")); + assertAddToPossibleOptions(transformOptionGroup, set("2", "4.1"), + set("1", "2", "4.1", "4.2", "4.3"), set("2", "4.2")); + assertAddToPossibleOptions(transformOptionGroup, set("2", "4.2"), + set("1", "2", "4.1", "4.2", "4.3"), set("2", "4.2")); + } + + @Test + public void testRequiredGroup() + { + TransformOptionGroup transformOptionGroup = + new TransformOptionGroup(true, set( + new TransformOptionValue(false, "1"), + new TransformOptionValue(true, "2"), + new TransformOptionGroup(false, set( + new TransformOptionValue(false, "3.1"), + new TransformOptionValue(false, "3.2"), + new TransformOptionValue(false, "3.3"))), + new TransformOptionGroup(true, set( + new TransformOptionValue(false, "4.1"), + new TransformOptionValue(true, "4.2"), + new TransformOptionValue(false, "4.3"))))); + + assertAddToPossibleOptions(transformOptionGroup, emptySet(), + set("1", "2", "4.1", "4.2", "4.3"), set("2", "4.2")); + assertAddToPossibleOptions(transformOptionGroup, set("1"), + set("1", "2", "4.1", "4.2", "4.3"), set("2", "4.2")); + assertAddToPossibleOptions(transformOptionGroup, set("2", "3.2"), + set("1", "2", "3.1", "3.2", "3.3", "4.1", "4.2", "4.3"), set("2", "4.2")); + assertAddToPossibleOptions(transformOptionGroup, set("2", "4.1"), + set("1", "2", "4.1", "4.2", "4.3"), set("2", "4.2")); + assertAddToPossibleOptions(transformOptionGroup, set("2", "4.2"), + set("1", "2", "4.1", "4.2", "4.3"), set("2", "4.2")); + } + + @Test + public void testNestedGroups() + { + TransformOptionGroup transformOptionGroup = + new TransformOptionGroup(false, set( + new TransformOptionGroup(false, set( + new TransformOptionValue(false, "1"), + new TransformOptionGroup(false, set( + new TransformOptionValue(false, "1.2"), + new TransformOptionGroup(false, set( + new TransformOptionValue(false, "1.2.3"))))))), + new TransformOptionGroup(false, set( + new TransformOptionValue(false, "2"), + new TransformOptionGroup(false, set( + new TransformOptionValue(false, "2.2"), + new TransformOptionGroup(false, set( + new TransformOptionGroup(false, set( + new TransformOptionValue(false, "2.2.1.2"))))))))), + new TransformOptionGroup(false, set( + new TransformOptionValue(true, "3"), + new TransformOptionGroup(false, set( + new TransformOptionGroup(false, set( + new TransformOptionGroup(false, set( + new TransformOptionValue(false, "3.1.1.2"))))))))), + new TransformOptionGroup(false, set( + new TransformOptionValue(false, "4"), + new TransformOptionGroup(true, set( + new TransformOptionGroup(false, set( + new TransformOptionGroup(false, set( + new TransformOptionValue(false, "4.1.1.2"))))))))), + new TransformOptionGroup(false, set( + new TransformOptionValue(false, "5"), + new TransformOptionGroup(false, set( + new TransformOptionGroup(true, set( + new TransformOptionGroup(false, set( + new TransformOptionValue(false, "5.1.1.2"))))))))), + new TransformOptionGroup(false, set( + new TransformOptionValue(false, "6"), + new TransformOptionGroup(false, set( + new TransformOptionGroup(false, set( + new TransformOptionGroup(true, set( + new TransformOptionValue(false, "6.1.1.2"))))))))), + new TransformOptionGroup(false, set( + new TransformOptionValue(false, "7"), + new TransformOptionGroup(false, set( + new TransformOptionGroup(false, set( + new TransformOptionGroup(false, set( + new TransformOptionValue(true, "7.1.1.2"))))))))) + )); + + assertAddToPossibleOptions(transformOptionGroup, emptySet(), + emptySet(), emptySet()); + assertAddToPossibleOptions(transformOptionGroup, set("1"), + set("1"), emptySet()); + assertAddToPossibleOptions(transformOptionGroup, set("1", "7"), + set("1", "7"), emptySet()); + assertAddToPossibleOptions(transformOptionGroup, set("1", "7.1.1.2"), + set("1", "7", "7.1.1.2"), set("7.1.1.2")); + assertAddToPossibleOptions(transformOptionGroup, set("1", "6"), + set("1", "6"), emptySet()); + assertAddToPossibleOptions(transformOptionGroup, set("1", "6.1.1.2"), + set("1", "6", "6.1.1.2"), emptySet()); + assertAddToPossibleOptions(transformOptionGroup, set("1", "5"), + set("1", "5"), emptySet()); + assertAddToPossibleOptions(transformOptionGroup, set("1", "5.1.1.2"), + set("1", "5", "5.1.1.2"), emptySet()); + assertAddToPossibleOptions(transformOptionGroup, set("1", "4"), + set("1", "4"), emptySet()); + assertAddToPossibleOptions(transformOptionGroup, set("1", "4.1.1.2"), + set("1", "4", "4.1.1.2"), emptySet()); + assertAddToPossibleOptions(transformOptionGroup, set("1", "3"), + set("1", "3"), set("3")); + assertAddToPossibleOptions(transformOptionGroup, set("1", "3.1.1.2"), + set("1", "3", "3.1.1.2"), set("3")); + + assertAddToPossibleOptions(transformOptionGroup, set("2"), + set("2"), emptySet()); + assertAddToPossibleOptions(transformOptionGroup, set("2", "2.2"), + set("2", "2.2"), emptySet()); + assertAddToPossibleOptions(transformOptionGroup, set("3"), + set("3"), set("3")); + assertAddToPossibleOptions(transformOptionGroup, set("3.1.1.2"), + set("3", "3.1.1.2"), set("3")); + } + + @Test + public void testRegistryIsSupportedMethod() + { + assertIsSupported(set("a"), set("a", "B", "c"), "required option B is missing"); + assertIsSupported(emptySet(), set("a", "B", "c"), "required option B is missing"); + assertIsSupported(set("B"), set("a", "B", "c"), null); + assertIsSupported(set("B", "c"), set("a", "B", "c"), null); + assertIsSupported(set("B", "a", "c"), set("a", "B", "c"), null); + + assertIsSupported(set("B", "d"), set("a", "B", "c"), "there is an extra option d"); + assertIsSupported(set("B", "c", "d"), set("a", "B", "c"), "there is an extra option d"); + assertIsSupported(set("d"), set("a", "B", "c"), + "required option B is missing and there is an extra option d"); + + assertIsSupported(set("a"), set("a", "b", "c"), null); + assertIsSupported(emptySet(), set("a", "b", "c"), null); + assertIsSupported(set("a", "b", "c"), set("a", "b", "c"), null); + } + + @Test + public void testNoActualOptions() throws Exception + { + assertTransformOptions(set( + new TransformOptionValue(false, "option1"), + new TransformOptionValue(false, "option2"))); + } + + @Test + public void testNoTransformOptions() throws Exception + { + assertTransformOptions(emptySet()); + assertTransformOptions(null); + } + + @Test + public void testSupported() throws Exception + { + mapOfTransformOptions.put("options1", set( + new TransformOptionValue(false, "page"), + new TransformOptionValue(false, "width"), + new TransformOptionValue(false, "height"))); + final Transformer transformer = new Transformer("name", singleton("options1"), set( + SupportedSourceAndTarget.builder() + .withSourceMediaType(DOC) + .withTargetMediaType(GIF) + .withMaxSourceSizeBytes(102400L) + .build(), + SupportedSourceAndTarget.builder() + .withSourceMediaType(DOC) + .withTargetMediaType(JPEG) + .build(), + SupportedSourceAndTarget.builder() + .withSourceMediaType(MSG) + .withTargetMediaType(GIF) + .build())); + + assertSupported(transformer, DOC, 1024, GIF, emptyMap(), null); + assertSupported(transformer, DOC, 102400, GIF, emptyMap(), null); + assertSupported(transformer, DOC, 102401, GIF, emptyMap(), "source is too large"); + assertSupported(transformer, DOC, 1024, JPEG, emptyMap(), null); + assertSupported(transformer, GIF, 1024, DOC, emptyMap(), + GIF + " is not a source of this transformer"); + assertSupported(transformer, MSG, 1024, GIF, emptyMap(), null); + assertSupported(transformer, MSG, 1024, JPEG, emptyMap(), + MSG + " to " + JPEG + " is not supported by this transformer"); + + assertSupported(transformer, DOC, 1024, GIF, buildActualOptions(set("page", "width")), + null); + assertSupported(transformer, DOC, 1024, GIF, + buildActualOptions(set("page", "width", "startPage")), "startPage is not an option"); + } + + @Test + // renditionName used as the cache key, is an alias for a set of actualOptions and the target mimetype. + // The source mimetype may change. + public void testCache() + { + mapOfTransformOptions.put("options1", set( + new TransformOptionValue(false, "page"), + new TransformOptionValue(false, "width"), + new TransformOptionValue(false, "height"))); + + final Transformer transformer = new Transformer("name", singleton("options1"), set( + SupportedSourceAndTarget.builder() + .withSourceMediaType(DOC) + .withTargetMediaType(GIF) + .withMaxSourceSizeBytes(102400L) + .build(), + SupportedSourceAndTarget.builder() + .withSourceMediaType(MSG) + .withTargetMediaType(GIF) + .build())); + + TransformConfig transformConfig = TransformConfig.builder() + .withTransformers(Collections.singletonList(transformer)) + .withTransformOptions(mapOfTransformOptions) + .build(); + CombinedTransformConfig.combineAndRegister(transformConfig, getClass().getName(), getBaseUrl(transformer), registry); + + assertSupported(DOC, 1024, GIF, emptyMap(), "doclib", ""); + assertSupported(MSG, 1024, GIF, emptyMap(), "doclib", ""); + + assertEquals(102400L, registry.findMaxSize(DOC, GIF, emptyMap(), "doclib")); + assertEquals(-1L, registry.findMaxSize(MSG, GIF, emptyMap(), "doclib")); + + // check we are now using the cached value. + final SupportedTransform cachedSupportedTransform = new SupportedTransform("name1", + emptySet(), 999999L, 0); + + registry.getData() + .retrieveCached("doclib", DOC) + .add(cachedSupportedTransform); + assertEquals(999999L, registry.findMaxSize(DOC, GIF, emptyMap(), "doclib")); + } + + @Test + public void testGetTransformerName() throws Exception + { + Transformer t1 = newTransformer("transformer1", MSG, GIF, 100, 50); + Transformer t2 = newTransformer("transformer2", MSG, GIF, 200, 60); + Transformer t3 = newTransformer("transformer3", MSG, GIF, 200, 40); + Transformer t4 = newTransformer("transformer4", MSG, GIF, -1, 100); + Transformer t5 = newTransformer("transformer5", MSG, GIF, -1, 80); + + // Select on size - priority is ignored + assertTransformerName(MSG, 100, GIF, emptyMap(), "transformer1", t1, t2); + assertTransformerName(MSG, 150, GIF, emptyMap(), "transformer2", t1, t2); + assertTransformerName(MSG, 250, GIF, emptyMap(), null, t1, t2); + // Select on priority - t1, t2 and t4 are discarded. + // t3 is a higher priority and has a larger size than t1 and t2. + // Similar story fo t4 with t5. + assertTransformerName(MSG, 100, GIF, emptyMap(), "transformer3", t1, t2, t3, t4, t5); + assertTransformerName(MSG, 200, GIF, emptyMap(), "transformer3", t1, t2, t3, t4, t5); + // Select on size and priority, t1 and t2 discarded + assertTransformerName(MSG, 200, GIF, emptyMap(), "transformer3", t1, t2, t3, t4); + assertTransformerName(MSG, 300, GIF, emptyMap(), "transformer4", t1, t2, t3, t4); + assertTransformerName(MSG, 300, GIF, emptyMap(), "transformer5", t1, t2, t3, t4, t5); + } + + private Transformer newTransformer(String transformerName, String sourceMediaType, String targetMediaType, + long maxSourceSizeBytes, int priority) + { + return Transformer.builder().withTransformerName(transformerName) + .withSupportedSourceAndTargetList(ImmutableSet.of( + SupportedSourceAndTarget.builder() + .withSourceMediaType(sourceMediaType) + .withTargetMediaType(targetMediaType) + .withMaxSourceSizeBytes(maxSourceSizeBytes) + .withPriority(priority) + .build())) + .build(); + } + + @Test + public void testMultipleTransformers() throws Exception + { + mapOfTransformOptions.put("options1", set( + new TransformOptionValue(false, "page"), + new TransformOptionValue(false, "width"), + new TransformOptionValue(false, "height"))); + mapOfTransformOptions.put("options2", set( + new TransformOptionValue(false, "opt1"), + new TransformOptionValue(false, "opt2"))); + mapOfTransformOptions.put("options3", new HashSet<>(singletonList( + new TransformOptionValue(false, "opt1")))); + + Transformer transformer1 = new Transformer("transformer1", singleton("options1"), set( + SupportedSourceAndTarget.builder() + .withSourceMediaType(DOC) + .withTargetMediaType(GIF) + .withMaxSourceSizeBytes(102400L) + .build(), + SupportedSourceAndTarget.builder() + .withSourceMediaType(DOC) + .withTargetMediaType(JPEG) + .build(), + SupportedSourceAndTarget.builder() + .withSourceMediaType(MSG) + .withTargetMediaType(GIF) + .build())); + + Transformer transformer2 = new Transformer("transformer2", singleton("options2"), set( + SupportedSourceAndTarget.builder() + .withSourceMediaType(PDF) + .withTargetMediaType(GIF) + .build(), + SupportedSourceAndTarget.builder() + .withSourceMediaType(PPT) + .withTargetMediaType(JPEG) + .build())); + + Transformer transformer3 = new Transformer("transformer3", singleton("options3"), + new HashSet(singletonList(SupportedSourceAndTarget.builder() + .withSourceMediaType(DOC) + .withTargetMediaType(GIF) + .build()))); + + assertSupported(DOC, 1024, GIF, emptyMap(), null, transformer1); + assertSupported(DOC, 1024, GIF, emptyMap(), null, transformer1, transformer2); + assertSupported(DOC, 1024, GIF, emptyMap(), null, transformer1, transformer2, + transformer3); + + assertSupported(DOC, 102401, GIF, emptyMap(), "source is too large", transformer1); + assertSupported(DOC, 102401, GIF, emptyMap(), null, transformer1, transformer3); + + assertSupported(PDF, 1024, GIF, emptyMap(), "Only transformer2 supports these mimetypes", + transformer1); + assertSupported(PDF, 1024, GIF, emptyMap(), null, transformer1, transformer2); + assertSupported(PDF, 1024, GIF, emptyMap(), null, transformer1, transformer2, + transformer3); + + final Map actualOptions = buildActualOptions(set("opt1")); + assertSupported(PDF, 1024, GIF, actualOptions, "Only transformer2/4 supports these options", + transformer1); + assertSupported(PDF, 1024, GIF, actualOptions, null, transformer1, transformer2); + assertSupported(PDF, 1024, GIF, actualOptions, null, transformer1, transformer2, + transformer3); + assertSupported(PDF, 1024, GIF, actualOptions, + "transformer4 supports opt1 but not the source mimetype ", transformer1, transformer3); + } + + @SafeVarargs + private static Set set(T... elements) + { + if (elements == null || elements.length == 0) + { + return emptySet(); + } + return ImmutableSet.copyOf(elements); + } +} diff --git a/alfresco-transform-model/src/test/java/org/alfresco/transform/router/TransformStackTest.java b/alfresco-transform-model/src/test/java/org/alfresco/transform/router/TransformStackTest.java new file mode 100644 index 00000000..0e4e5677 --- /dev/null +++ b/alfresco-transform-model/src/test/java/org/alfresco/transform/router/TransformStackTest.java @@ -0,0 +1,537 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.router; + +import com.google.common.collect.ImmutableMap; +import org.alfresco.transform.client.model.InternalContext; +import org.alfresco.transform.client.model.MultiStep; +import org.alfresco.transform.client.model.TransformReply; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Map; +import java.util.UUID; + +import static org.alfresco.transform.router.TransformStack.OPTIONS_LEVEL; +import static org.alfresco.transform.router.TransformStack.SEPARATOR; +import static org.alfresco.transform.router.TransformStack.TOP_STACK_LEVEL; +import static org.alfresco.transform.router.TransformStack.getInitialSourceReference; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.Mockito.doReturn; + +class TransformStackTest +{ + private static long START = System.currentTimeMillis(); + private static String STEP = SEPARATOR + "name" + SEPARATOR + "source" + SEPARATOR + "target"; + + @Mock + private TransformerDebug transformerDebug; + + @Mock + private TransformReply reply; + + // Note: If you change this also change AbstractRouterTest.testNestedTransform as they match + public static final ImmutableMap TEST_LEVELS = ImmutableMap.of( + "top", TransformStack.levelBuilder(TransformStack.PIPELINE_FLAG) + .withStep("pipeline 1-N", "type1", "typeN"), + "pipeline 1-N", TransformStack.levelBuilder(TransformStack.PIPELINE_FLAG) + .withStep("transform1-2", "type1", "type2") + .withStep("pipeline 2-3", "type2", "type3") + .withStep("failover 3-N", "type3", "typeN"), + "pipeline 2-3", TransformStack.levelBuilder(TransformStack.PIPELINE_FLAG) + .withStep("transform2-4", "type2", "type4") + .withStep("transform4-3", "type4", "type3"), + "failover 3-N", TransformStack.levelBuilder(TransformStack.FAILOVER_FLAG) + .withStep("transform3-Na", "type3", "typeN") + .withStep("transform3-Nb", "type3", "typeN") + .withStep("pipeline 3-Nc", "type3", "typeN"), + "pipeline 3-Nc", TransformStack.levelBuilder(TransformStack.PIPELINE_FLAG) + .withStep("transform3-5", "type3", "type5") + .withStep("pipeline 5-N", "type5", "typeN"), + "pipeline 5-N", TransformStack.levelBuilder(TransformStack.PIPELINE_FLAG) + .withStep("transform5-6", "type5", "type6") + .withStep("transform6-N", "type6", "typeN")); + private final InternalContext internalContext = new InternalContext(); + private final Map options = ImmutableMap.of("key1", "value1", "key2", "", "key3", "value3"); + private final String sourceReference = UUID.randomUUID().toString(); + + @BeforeEach + void setUp() + { + MockitoAnnotations.openMocks(this); + + // Repeat what is done by Router.initialiseContext + internalContext.setMultiStep(new MultiStep()); + internalContext.getMultiStep().setTransformsToBeDone(new ArrayList<>()); + TransformStack.setInitialTransformRequestOptions(internalContext, options); + TransformStack.setInitialSourceReference(internalContext, sourceReference); + doReturn(internalContext).when(reply).getInternalContext(); + } + + @Test + public void testOptions() + { + assertEquals(options, TransformStack.getInitialTransformRequestOptions(internalContext)); + } + + @Test + public void testOptionsEmpty() + { + ImmutableMap options = ImmutableMap.of(); + + TransformStack.setInitialTransformRequestOptions(internalContext, options); + assertEquals(options, TransformStack.getInitialTransformRequestOptions(internalContext)); + } + + @Test + public void testOptionsEmptyLastValue() + { + ImmutableMap options = ImmutableMap.of("key1", "value1", "key2", ""); + + TransformStack.setInitialTransformRequestOptions(internalContext, options); + assertEquals(options, TransformStack.getInitialTransformRequestOptions(internalContext)); + } + + @Test + public void testSourceReference() + { + assertEquals(sourceReference, getInitialSourceReference(internalContext)); + } + + @Test + public void testSourceReferenceNull() + { + TransformStack.setInitialSourceReference(internalContext, null); + assertEquals(null, getInitialSourceReference(internalContext)); + } + + @Test + public void testSourceReferenceBlank() + { + TransformStack.setInitialSourceReference(internalContext, ""); + assertEquals("", getInitialSourceReference(internalContext)); + } + + @Test + public void testLevelBuilder() + { + assertEquals("P⏐1⏐0⏐0⏐pipeline 1-N⏐type1⏐typeN", TEST_LEVELS.get("top").build()); + assertEquals("P⏐1⏐0⏐0⏐failover 3-N⏐type3⏐typeN⏐pipeline 2-3⏐type2⏐type3⏐transform1-2⏐type1⏐type2", TEST_LEVELS.get("pipeline 1-N").build()); + assertEquals("F⏐1⏐0⏐0⏐pipeline 3-Nc⏐type3⏐typeN⏐transform3-Nb⏐type3⏐typeN⏐transform3-Na⏐type3⏐typeN", TEST_LEVELS.get("failover 3-N").build()); + } + + @Test + public void testAttemptedRetries() + { + TransformStack.addTransformLevel(internalContext, TEST_LEVELS.get("top")); + assertEquals(0, TransformStack.getAttemptedRetries(internalContext)); + + TransformStack.incrementAttemptedRetries(internalContext); + assertEquals(1, TransformStack.getAttemptedRetries(internalContext)); + + TransformStack.incrementAttemptedRetries(internalContext); + assertEquals(2, TransformStack.getAttemptedRetries(internalContext)); + + TransformStack.resetAttemptedRetries(internalContext); + assertEquals(0, TransformStack.getAttemptedRetries(internalContext)); + } + + @Test + public void testReference() + { + TransformStack.addTransformLevel(internalContext, TEST_LEVELS.get("top")); + assertEquals("1", TransformStack.getReference(internalContext)); + + TransformStack.setReference(internalContext, 123); + assertEquals("123", TransformStack.getReference(internalContext)); + + TransformStack.addTransformLevel(internalContext, TEST_LEVELS.get("pipeline 1-N")); + assertEquals("123.1", TransformStack.getReference(internalContext)); + + TransformStack.incrementReference(internalContext); + assertEquals("123.2", TransformStack.getReference(internalContext)); + + TransformStack.addTransformLevel(internalContext, TEST_LEVELS.get("pipeline 2-3")); + assertEquals("123.2.1", TransformStack.getReference(internalContext)); + + TransformStack.removeTransformLevel(internalContext); + TransformStack.incrementReference(internalContext); + assertEquals("123.3", TransformStack.getReference(internalContext)); + + TransformStack.removeTransformLevel(internalContext); + assertEquals("123", TransformStack.getReference(internalContext)); + } + + @Test + public void testSetReferenceInADummyTopLevelIfUnset() + { + // Used when creating a TransformRouterException prior to setting the reference + + // Undo setup() + internalContext.getMultiStep().setTransformsToBeDone(new ArrayList<>()); + + TransformStack.setReferenceInADummyTopLevelIfUnset(internalContext, 23); + assertEquals("23", TransformStack.getReference(internalContext)); + } + + @Test + public void testReplicateWorkflowWithSuccess() + { + replicateWorkflowStepsPriorToFailureOrSuccess(); + + // Assume a successful transform, so successful there should be no more steps or levels after this + TransformStack.removeSuccessfulStep(reply, transformerDebug); + assertTrue(TransformStack.isFinished(internalContext)); + } + + @Test + // Tests the failure on a transform indirectly (it is a step in a pipeline) under a failover transformer. + public void testReplicateWorkflowWithFailure() + { + replicateWorkflowStepsPriorToFailureOrSuccess(); + + // Assume a transform failure. While loop should remove the 2 indirect pipeline levels + int removedLevels = 0; + while (!TransformStack.isParentAFailover(internalContext)) + { + removedLevels++; + TransformStack.removeTransformLevel(internalContext); + } + assertEquals(2, removedLevels); + assertTrue(TransformStack.isLastStepInTransformLevel(internalContext)); + TransformStack.removeFailedStep(reply, transformerDebug); // Should remove the rest as failure was last step in failover + assertTrue(TransformStack.isFinished(internalContext)); + } + + // Replicates the sequence of TransformStack method calls for a workflow. Steps through each transform, with + // some failures in a failover transformer, before returning to allow the calling test method to either fail the + // next step or not. + private void replicateWorkflowStepsPriorToFailureOrSuccess() + { + // Initial transform request, so add a level + TransformStack.addTransformLevel(internalContext, TEST_LEVELS.get("top")); + assertFalse(TransformStack.isFinished(internalContext)); + TransformStack.Step step = TransformStack.currentStep(internalContext); + + assertEquals("pipeline 1-N", step.getTransformerName()); + assertEquals("type1", step.getSourceMediaType()); + assertEquals("typeN", step.getTargetMediaType()); + assertEquals(null, TransformStack.getParentName(internalContext)); + assertFalse(TransformStack.isParentAFailover(internalContext)); + + // Because it is a pipeline, add a level + TransformStack.addTransformLevel(internalContext, TEST_LEVELS.get("pipeline 1-N")); + assertFalse(TransformStack.isFinished(internalContext)); + step = TransformStack.currentStep(internalContext); + + assertEquals("transform1-2", step.getTransformerName()); + assertEquals("type1", step.getSourceMediaType()); + assertEquals("type2", step.getTargetMediaType()); + assertEquals("pipeline 1-N", TransformStack.getParentName(internalContext)); + assertFalse(TransformStack.isParentAFailover(internalContext)); + + // Assume a successful transform, so move on to next step add a level + TransformStack.removeSuccessfulStep(reply, transformerDebug); + assertFalse(TransformStack.isFinished(internalContext)); + step = TransformStack.currentStep(internalContext); + + assertEquals("pipeline 2-3", step.getTransformerName()); + assertEquals("type2", step.getSourceMediaType()); + assertEquals("type3", step.getTargetMediaType()); + assertEquals("pipeline 1-N", TransformStack.getParentName(internalContext)); + assertFalse(TransformStack.isParentAFailover(internalContext)); + + // Because it is a pipeline, add a level + TransformStack.addTransformLevel(internalContext, TEST_LEVELS.get("pipeline 2-3")); + assertFalse(TransformStack.isFinished(internalContext)); + step = TransformStack.currentStep(internalContext); + + assertEquals("transform2-4", step.getTransformerName()); + assertEquals("type2", step.getSourceMediaType()); + assertEquals("type4", step.getTargetMediaType()); + assertEquals("pipeline 2-3", TransformStack.getParentName(internalContext)); + assertFalse(TransformStack.isParentAFailover(internalContext)); + + // Assume a successful transform, so move on to next step add a level + TransformStack.removeSuccessfulStep(reply, transformerDebug); + assertFalse(TransformStack.isFinished(internalContext)); + step = TransformStack.currentStep(internalContext); + + assertEquals("transform4-3", step.getTransformerName()); + assertEquals("type4", step.getSourceMediaType()); + assertEquals("type3", step.getTargetMediaType()); + assertEquals("pipeline 2-3", TransformStack.getParentName(internalContext)); + assertFalse(TransformStack.isParentAFailover(internalContext)); + + // Assume a successful transform, so move on to next step add a level + TransformStack.removeSuccessfulStep(reply, transformerDebug); + assertFalse(TransformStack.isFinished(internalContext)); + step = TransformStack.currentStep(internalContext); + + assertEquals("failover 3-N", step.getTransformerName()); + assertEquals("type3", step.getSourceMediaType()); + assertEquals("typeN", step.getTargetMediaType()); + assertEquals("pipeline 1-N", TransformStack.getParentName(internalContext)); + assertFalse(TransformStack.isParentAFailover(internalContext)); + + // Because it is a failover, add a level + TransformStack.addTransformLevel(internalContext, TEST_LEVELS.get("failover 3-N")); + step = TransformStack.currentStep(internalContext); + + assertEquals("transform3-Na", step.getTransformerName()); + assertEquals("type3", step.getSourceMediaType()); + assertEquals("typeN", step.getTargetMediaType()); + assertEquals("failover 3-N", TransformStack.getParentName(internalContext)); + assertTrue(TransformStack.isParentAFailover(internalContext)); + + // Assume 1st failover step fails + while (!TransformStack.isParentAFailover(internalContext)) + { + TransformStack.removeTransformLevel(internalContext); + } + assertFalse(TransformStack.isLastStepInTransformLevel(internalContext)); + TransformStack.removeFailedStep(reply, transformerDebug); + assertFalse(TransformStack.isFinished(internalContext)); + step = TransformStack.currentStep(internalContext); + + assertEquals("transform3-Nb", step.getTransformerName()); + assertEquals("type3", step.getSourceMediaType()); + assertEquals("typeN", step.getTargetMediaType()); + assertEquals("failover 3-N", TransformStack.getParentName(internalContext)); + assertTrue(TransformStack.isParentAFailover(internalContext)); + + // Assume 2nd failover step fails + while (!TransformStack.isParentAFailover(internalContext)) + { + TransformStack.removeTransformLevel(internalContext); + } + assertFalse(TransformStack.isLastStepInTransformLevel(internalContext)); + TransformStack.removeFailedStep(reply, transformerDebug); + assertFalse(TransformStack.isFinished(internalContext)); + step = TransformStack.currentStep(internalContext); + + assertEquals("pipeline 3-Nc", step.getTransformerName()); + assertEquals("type3", step.getSourceMediaType()); + assertEquals("typeN", step.getTargetMediaType()); + assertEquals("failover 3-N", TransformStack.getParentName(internalContext)); + assertTrue(TransformStack.isParentAFailover(internalContext)); + + // Because it is a pipeline, add a level + TransformStack.addTransformLevel(internalContext, TEST_LEVELS.get("pipeline 3-Nc")); + assertFalse(TransformStack.isFinished(internalContext)); + step = TransformStack.currentStep(internalContext); + + assertEquals("transform3-5", step.getTransformerName()); + assertEquals("type3", step.getSourceMediaType()); + assertEquals("type5", step.getTargetMediaType()); + assertEquals("pipeline 3-Nc", TransformStack.getParentName(internalContext)); + assertFalse(TransformStack.isParentAFailover(internalContext)); + + // Assume a successful transform, so move on to next step add a level + TransformStack.removeSuccessfulStep(reply, transformerDebug); + assertFalse(TransformStack.isFinished(internalContext)); + step = TransformStack.currentStep(internalContext); + + assertEquals("pipeline 5-N", step.getTransformerName()); + assertEquals("type5", step.getSourceMediaType()); + assertEquals("typeN", step.getTargetMediaType()); + assertEquals("pipeline 3-Nc", TransformStack.getParentName(internalContext)); + assertFalse(TransformStack.isParentAFailover(internalContext)); + + // Because it is a pipeline, add a level + TransformStack.addTransformLevel(internalContext, TEST_LEVELS.get("pipeline 5-N")); + assertFalse(TransformStack.isFinished(internalContext)); + step = TransformStack.currentStep(internalContext); + + assertEquals("transform5-6", step.getTransformerName()); + assertEquals("type5", step.getSourceMediaType()); + assertEquals("type6", step.getTargetMediaType()); + assertEquals("pipeline 5-N", TransformStack.getParentName(internalContext)); + assertFalse(TransformStack.isParentAFailover(internalContext)); + + // Assume a successful transform, so move on to next step add a level + TransformStack.removeSuccessfulStep(reply, transformerDebug); + assertFalse(TransformStack.isFinished(internalContext)); + step = TransformStack.currentStep(internalContext); + + assertEquals("transform6-N", step.getTransformerName()); + assertEquals("type6", step.getSourceMediaType()); + assertEquals("typeN", step.getTargetMediaType()); + assertEquals("pipeline 5-N", TransformStack.getParentName(internalContext)); + assertFalse(TransformStack.isParentAFailover(internalContext)); + } + + @Test + // Tests a workflow where all transforms are successful, using a loop. + public void testWorkflowWithLoop() + { + TransformStack.addTransformLevel(internalContext, TEST_LEVELS.get("top")); + int transformStepCount = 0; + do + { + transformStepCount++; + TransformStack.Step step = TransformStack.currentStep(internalContext); + String transformerName = step.getTransformerName(); + int depth = internalContext.getMultiStep().getTransformsToBeDone().size() - 2; + System.out.println(transformStepCount + " ".substring(0, depth*2+1) + transformerName); + TransformStack.LevelBuilder nextLevel = TEST_LEVELS.get(transformerName); + if (nextLevel == null) + { + TransformStack.removeSuccessfulStep(reply, transformerDebug); + } + else + { + TransformStack.addTransformLevel(internalContext, nextLevel); + } + if (transformStepCount >= 25) + { + fail("Appear to be in an infinite loop"); + } + } while (!TransformStack.isFinished(internalContext)); + assertEquals(7, transformStepCount); + } + + @Test + public void testCheckStructureNoOptions() + { + internalContext.getMultiStep().setTransformsToBeDone(new ArrayList<>()); + + assertEquals("T-Reply InternalContext did not have the Stack set", + TransformStack.checkStructure(internalContext, "T-Reply")); + } + + @Test + public void testCheckStructureNoSourceRef() + { + internalContext.getMultiStep().setTransformsToBeDone(new ArrayList<>()); + TransformStack.setInitialTransformRequestOptions(internalContext, options); + + assertEquals("T-Request InternalContext did not have the Stack set", + TransformStack.checkStructure(internalContext, "T-Request")); + } + + @Test + public void testCheckStructureNoStack() + { + internalContext.getMultiStep().setTransformsToBeDone(new ArrayList<>()); + TransformStack.setInitialTransformRequestOptions(internalContext, options); + TransformStack.setInitialSourceReference(internalContext, sourceReference); + + assertEquals("T-something InternalContext did not have the Stack set", + TransformStack.checkStructure(internalContext, "T-something")); + } + + @Test + public void testCheckStructureOptionsOk() + { + TransformStack.addTransformLevel(internalContext, TEST_LEVELS.get("top")); + + for (String value : Arrays.asList( + "", + "key1" + SEPARATOR + "value1", + "key1" + SEPARATOR + "value1" + SEPARATOR + "key2" + SEPARATOR + "value2")) + { + System.out.println("TransformOptions value: " + value); + internalContext.getMultiStep().getTransformsToBeDone().set(OPTIONS_LEVEL, value); + assertNull(TransformStack.checkStructure(internalContext, "T-Reply")); + // call the getter just in case we have missed something + TransformStack.getInitialTransformRequestOptions(internalContext); + } + } + + @Test + public void testCheckStructureOptionsBad() + { + TransformStack.addTransformLevel(internalContext, TEST_LEVELS.get("top")); + + for (String value : Arrays.asList( + null, + "noValue", + SEPARATOR + "noKey", + "key" + SEPARATOR + "value" + SEPARATOR + "noKey")) + { + System.out.println("TransformOptions value: " + value); + internalContext.getMultiStep().getTransformsToBeDone().set(OPTIONS_LEVEL, value); + assertEquals("T-Reply InternalContext did not have the TransformOptions set correctly", + TransformStack.checkStructure(internalContext, "T-Reply")); + } + } + + @Test + public void testCheckStructureStackLevelsOk() + { + TransformStack.addTransformLevel(internalContext, TEST_LEVELS.get("top")); + + for (String value : Arrays.asList( + "P" + SEPARATOR + "20" + SEPARATOR + START + SEPARATOR + "1" + STEP, + "P" + SEPARATOR + "4" + SEPARATOR + "123" + SEPARATOR + "12" + STEP + STEP)) + { + System.out.println("TransformLevel value: " + value); + internalContext.getMultiStep().getTransformsToBeDone().set(TOP_STACK_LEVEL, value); + assertNull(TransformStack.checkStructure(internalContext, "T-Reply")); + // call a getter just in case we have missed something + TransformStack.currentStep(internalContext); + }; + } + + @Test + public void testCheckStructureStackLevelsBad() + { + TransformStack.addTransformLevel(internalContext, TEST_LEVELS.get("top")); + + String MAX_INT_PLUS_1 = BigInteger.valueOf(Integer.MAX_VALUE + 1).toString(); + String MAX_LONG_PLUS_1 = BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE).toString(); + + for (String value : Arrays.asList( + null, + "", + "F" + SEPARATOR + "12" + SEPARATOR + START + SEPARATOR + "2", + "F" + SEPARATOR + "-1" + SEPARATOR + START + SEPARATOR + "2" + STEP, + "F" + SEPARATOR + "1" + SEPARATOR + "-3" + SEPARATOR + "2" + STEP, + "F" + SEPARATOR + "1" + SEPARATOR + START + SEPARATOR + "-2" + STEP, + "F" + SEPARATOR + "a" + SEPARATOR + START + SEPARATOR + "-2" + STEP, + "F" + SEPARATOR + "1" + SEPARATOR + START + SEPARATOR + "b" + STEP, + "P" + SEPARATOR + "0" + SEPARATOR + START + SEPARATOR + "12" + SEPARATOR + "name", + "P" + SEPARATOR + "0" + SEPARATOR + START + SEPARATOR + "12" + SEPARATOR + "name" + SEPARATOR + "source", + "P" + SEPARATOR + "0" + SEPARATOR + START + SEPARATOR + "12" + SEPARATOR + "name" + SEPARATOR + "source" + SEPARATOR + "", + "P" + SEPARATOR + "0" + SEPARATOR + START + SEPARATOR + "12" + SEPARATOR + "name" + SEPARATOR + "" + SEPARATOR + "target", + "F" + SEPARATOR + MAX_INT_PLUS_1 + SEPARATOR + START + SEPARATOR + "2" + STEP, + "F" + SEPARATOR + "1" + SEPARATOR + MAX_LONG_PLUS_1 + SEPARATOR + "2" + STEP, + "F" + SEPARATOR + "1" + SEPARATOR + START + SEPARATOR + MAX_INT_PLUS_1 + STEP + )) + { + System.out.println("TransformLevel value: " + value); + internalContext.getMultiStep().getTransformsToBeDone().set(TOP_STACK_LEVEL, value); + assertEquals("T-Reply InternalContext did not have levels set correctly", + TransformStack.checkStructure(internalContext, "T-Reply")); + }; + } +} \ No newline at end of file diff --git a/alfresco-transform-model/src/test/java/org/alfresco/transform/router/TransformerDebugTest.java b/alfresco-transform-model/src/test/java/org/alfresco/transform/router/TransformerDebugTest.java new file mode 100644 index 00000000..0c0116e4 --- /dev/null +++ b/alfresco-transform-model/src/test/java/org/alfresco/transform/router/TransformerDebugTest.java @@ -0,0 +1,218 @@ +/* + * #%L + * Alfresco Transform Model + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * #L% + */ +package org.alfresco.transform.router; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; +import com.google.common.collect.ImmutableMap; +import org.alfresco.transform.client.model.InternalContext; +import org.alfresco.transform.client.model.TransformReply; +import org.alfresco.transform.client.model.TransformRequest; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.StringJoiner; + +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_TEXT_PLAIN; +import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_WORD; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests TransformerDebug. AbstractRouterTest in the t-router contains more complete end to end tests. The tests in this + * class are a smoke test at the moment. + */ +class TransformerDebugTest +{ + private static String MIMETYPE_PDF = "application/pdf"; + + TransformerDebug transformerDebug = new TransformerDebug(); + + private StringJoiner transformerDebugOutput = new StringJoiner("\n"); + + public String getTransformerDebugOutput() + { + return transformerDebugOutput.toString() + .replaceAll(" [\\d,]+ ms", " -- ms"); + } + + private void twoStepTransform(boolean isTEngine, boolean fail, Level logLevel) + { + transformerDebug.setIsTEngine(isTEngine); + monitorLogs(logLevel); + + TransformRequest request = TransformRequest.builder() + .withSourceSize(1234L) + .withInternalContext(InternalContext.initialise(null)) + .build(); + TransformStack.setInitialSourceReference(request.getInternalContext(), "fileRef"); + + TransformReply reply = TransformReply.builder() + .withInternalContext(request.getInternalContext()) + .build(); + + TransformStack.addTransformLevel(request.getInternalContext(), + TransformStack.levelBuilder(TransformStack.PIPELINE_FLAG) + .withStep("wrapper", MIMETYPE_TEXT_PLAIN, MIMETYPE_PDF)); + transformerDebug.pushTransform(request); + + TransformStack.addTransformLevel(request.getInternalContext(), + TransformStack.levelBuilder(TransformStack.PIPELINE_FLAG) + .withStep("transformer1", MIMETYPE_TEXT_PLAIN, MIMETYPE_WORD) + .withStep("transformer2", MIMETYPE_WORD, MIMETYPE_PDF)); + transformerDebug.pushTransform(request); + transformerDebug.logOptions(request); + TransformStack.removeSuccessfulStep(reply, transformerDebug); + + request.setTransformRequestOptions(ImmutableMap.of("k1", "v1", "k2","v2")); + transformerDebug.pushTransform(request); + transformerDebug.logOptions(request); + if (fail) + { + reply.setErrorDetails("Dummy error"); + transformerDebug.logFailure(reply); + } + else + { + TransformStack.removeSuccessfulStep(reply, transformerDebug); + TransformStack.removeTransformLevel(reply.getInternalContext()); + } + } + + private void monitorLogs(Level logLevel) + { + Logger logger = (Logger)LoggerFactory.getLogger(TransformerDebug.class); + AppenderBase logAppender = new AppenderBase<>() + { + @Override + protected void append(ILoggingEvent iLoggingEvent) + { + transformerDebugOutput.add(iLoggingEvent.getMessage()); + } + }; + logAppender.setContext((LoggerContext)LoggerFactory.getILoggerFactory()); + logger.setLevel(logLevel); + logger.addAppender(logAppender); + logAppender.start(); + } + + @Test + void testRouterTwoStepTransform() + { + twoStepTransform(false, false, Level.DEBUG); + + assertEquals("" + + "1 txt pdf 1.2 KB wrapper\n" + + "1.1 txt doc transformer1\n" + + "1.2 doc pdf transformer2\n" + + "1.2 k1=\"v1\"\n" + + "1.2 k2=\"v2\"\n" + + "1 Finished in -- ms", + getTransformerDebugOutput()); + } + + @Test + void testRouterTwoStepTransformWithTrace() + { + twoStepTransform(false, false, Level.TRACE); + + // With trace there are "Finished" lines for nested transforms, like a T-Engine's debug but still without + // the size and rendition name + assertEquals("" + + "1 txt pdf 1.2 KB wrapper\n" + + "1.1 txt doc transformer1\n" + + "1.1 Finished in -- ms\n" + + "1.2 doc pdf transformer2\n" + + "1.2 k1=\"v1\"\n" + + "1.2 k2=\"v2\"\n" + + "1.2 Finished in -- ms\n" + + "1 Finished in -- ms", + getTransformerDebugOutput()); + } + + @Test + void testEngineTwoStepTransform() + { + twoStepTransform(true, false, Level.DEBUG); + + // Note the first and last lines would only ever be logged on the router, but the expected data includes + // the extra "Finished" lines, sizes and renditions (if set in client data). + assertEquals("" + + "1 txt pdf 1.2 KB wrapper\n" + + "1.1 txt doc 1.2 KB transformer1\n" + + "1.1 Finished in -- ms\n" + + "1.2 doc pdf 1.2 KB transformer2\n" + + "1.2 k1=\"v1\"\n" + + "1.2 k2=\"v2\"\n" + + "1.2 Finished in -- ms\n" + + "1 Finished in -- ms", + getTransformerDebugOutput()); + } + + @Test + void testRouterTwoStepTransformWithFailure() + { + twoStepTransform(false, true, Level.DEBUG); + + assertEquals("" + + "1 txt pdf 1.2 KB wrapper\n" + + "1.1 txt doc transformer1\n" + + "1.2 doc pdf transformer2\n" + + "1.2 k1=\"v1\"\n" + + "1.2 k2=\"v2\"\n" + + "1.2 Dummy error", + getTransformerDebugOutput()); + } + + @Test + void testLogFailure() + { + monitorLogs(Level.TRACE); + + TransformReply reply = TransformReply.builder() + .withInternalContext(InternalContext.initialise(null)) + .withErrorDetails("T-Request was null - a major error") + .build(); + + transformerDebug.logFailure(reply); + + assertEquals(" T-Request was null - a major error", getTransformerDebugOutput()); + } + + @Test + void tesGetOptionAndValue() + { + String sixtyChars = "12345678 10 345678 20 345678 30 345678 40 345678 50 abcdefgh"; + String sixtyOneChars = "12345678 10 345678 20 345678 30 345678 40 345678 50 abcd12345"; + String expected = "12345678 10 345678 20 345678 30 345678 40 345678 50 ...12345"; + + assertEquals("ref key=\"value\"", + transformerDebug.getOptionAndValue("ref", "key", "value")); + assertEquals("ref key=\""+sixtyChars+"\"", + transformerDebug.getOptionAndValue("ref", "key", sixtyChars)); + assertEquals("ref key=\""+expected+"\"", + transformerDebug.getOptionAndValue("ref", "key", sixtyOneChars)); + } +} \ No newline at end of file diff --git a/alfresco-transformer-base/pom.xml b/alfresco-transformer-base/pom.xml index 3686023f..e5361d33 100644 --- a/alfresco-transformer-base/pom.xml +++ b/alfresco-transformer-base/pom.xml @@ -51,6 +51,7 @@ org.alfresco alfresco-transform-model + ${project.version} org.springframework.boot diff --git a/docs/build-and-release.md b/docs/build-and-release.md index 951d5b54..314f7bff 100644 --- a/docs/build-and-release.md +++ b/docs/build-and-release.md @@ -24,7 +24,7 @@ Travis CI builds differ by branch: include the _Release_ stage; - PR builds where the latest commit contains the `[trigger release]` tag will execute dry runs of the release jobs (no artifacts will be published until the PR is actually merged). -* `ATS-*` branches: +* `ATS-*` / `ACS-*` branches: - regular builds which include only the _Build_ and _Tests_ stages; All other branches are ignored. diff --git a/pom.xml b/pom.xml index 5def247b..51f549ef 100644 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,6 @@ 2.0.25 3.0.1.12 ${project.version} - 1.4.16 5.16.4 2.13.2 2.13.2.2 @@ -42,6 +41,7 @@ true + alfresco-transform-model alfresco-transformer-base alfresco-transform-imagemagick/alfresco-transform-imagemagick alfresco-transform-imagemagick/alfresco-transform-imagemagick-boot @@ -60,12 +60,14 @@ base + alfresco-transform-model alfresco-transformer-base imagemagick + alfresco-transform-model alfresco-transformer-base alfresco-transform-imagemagick/alfresco-transform-imagemagick alfresco-transform-imagemagick/alfresco-transform-imagemagick-boot @@ -74,6 +76,7 @@ libreoffice + alfresco-transform-model alfresco-transformer-base alfresco-transform-libreoffice/alfresco-transform-libreoffice alfresco-transform-libreoffice/alfresco-transform-libreoffice-boot @@ -82,6 +85,7 @@ misc + alfresco-transform-model alfresco-transformer-base alfresco-transform-misc/alfresco-transform-misc alfresco-transform-misc/alfresco-transform-misc-boot @@ -90,6 +94,7 @@ pdf-renderer + alfresco-transform-model alfresco-transformer-base alfresco-transform-pdf-renderer/alfresco-transform-pdf-renderer alfresco-transform-pdf-renderer/alfresco-transform-pdf-renderer-boot @@ -98,6 +103,7 @@ tika + alfresco-transform-model alfresco-transformer-base alfresco-transform-tika/alfresco-transform-tika alfresco-transform-tika/alfresco-transform-tika-boot @@ -107,6 +113,7 @@ aio-test + alfresco-transform-model alfresco-transformer-base alfresco-transform-core-aio/alfresco-transform-core-aio alfresco-transform-core-aio/alfresco-transform-core-aio-boot @@ -143,11 +150,6 @@ pdfbox-tools ${dependency.pdfbox.version} - - org.alfresco - alfresco-transform-model - ${dependency.alfresco-transform-model.version} - org.apache.cxf