From 9f2dd19e3658c00186d9a4ba65867f2d9e667d5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20=C3=85kesson?= Date: Thu, 26 Jun 2025 22:45:13 +0200 Subject: [PATCH 01/14] Draft of import options / parameters. --- .../databind/TransformImportOptions.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/main/java/se/simonsoft/cms/transform/config/databind/TransformImportOptions.java diff --git a/src/main/java/se/simonsoft/cms/transform/config/databind/TransformImportOptions.java b/src/main/java/se/simonsoft/cms/transform/config/databind/TransformImportOptions.java new file mode 100644 index 0000000..005fcf0 --- /dev/null +++ b/src/main/java/se/simonsoft/cms/transform/config/databind/TransformImportOptions.java @@ -0,0 +1,24 @@ +package se.simonsoft.cms.transform.config.databind; + +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) // Allow future changes. +public class TransformImportOptions { + + // private String type; // Not yet used, could be "xsl" in the future. + /* + * Params: + * - 'comment': History comment for the commit. + * - 'overwrite': Must be true in order to allow overwriting an existing item in CMS. + * + * - Future: 'TransformNN' + */ + private Map params = new HashMap<>(); + private String url; // Typically an http / https url, no authentication required. + private Map properties = new HashMap<>(); // Properties to set on the item. + + +} From 0a269f6956258feebadf1bc9adca880b14782bac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20=C3=85kesson?= Date: Thu, 7 Aug 2025 10:55:24 +0200 Subject: [PATCH 02/14] Defined import options for first implementation. --- .../databind/TransformImportOptions.java | 45 ++++++++++++++++++- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/src/main/java/se/simonsoft/cms/transform/config/databind/TransformImportOptions.java b/src/main/java/se/simonsoft/cms/transform/config/databind/TransformImportOptions.java index 005fcf0..2119965 100644 --- a/src/main/java/se/simonsoft/cms/transform/config/databind/TransformImportOptions.java +++ b/src/main/java/se/simonsoft/cms/transform/config/databind/TransformImportOptions.java @@ -7,8 +7,24 @@ @JsonIgnoreProperties(ignoreUnknown = true) // Allow future changes. public class TransformImportOptions { + + // NOTE: This class is equivalent to TransformConfigOptions, but for import (external data) instead of transform (already committed). - // private String type; // Not yet used, could be "xsl" in the future. + /* + * The itemId is a separate parameter. + * + * Folder: + * - The itemId is the folder where the item should be imported. + * - The folder must be configured with auto-naming when importing non-XML. + * - The folder can be configured with auto-naming when importing XML (with or without XSL transform). + * - A folder with auto-naming will only allow a primary output from XSL transform. + * - A folder without auto-naming will not allow a primary output from XSL transform. + * + * File: + * - The imported file will become that itemId (param 'overwrite' must be true if item already exists). + * - TBD: Will additional output be allowed from XSL transform? + */ + /* * Params: * - 'comment': History comment for the commit. @@ -17,8 +33,33 @@ public class TransformImportOptions { * - Future: 'TransformNN' */ private Map params = new HashMap<>(); - private String url; // Typically an http / https url, no authentication required. + private String url; // Typically an http / https url, no authentication required. Redirects must be followed. + private String content; // Content to import, typically XML or JSON. private Map properties = new HashMap<>(); // Properties to set on the item. + public Map getParams() { + return params; + } + public void setParams(Map params) { + this.params = params; + } + public String getUrl() { + return url; + } + public void setUrl(String url) { + this.url = url; + } + public String getContent() { + return content; + } + public void setContent(String content) { + this.content = content; + } + public Map getProperties() { + return properties; + } + public void setProperties(Map properties) { + this.properties = properties; + } } From 7ee0125d9269481eca3475c6919beab35d6ec8e6 Mon Sep 17 00:00:00 2001 From: Omid Manikhi Date: Thu, 21 Aug 2025 11:55:19 +0200 Subject: [PATCH 03/14] Implemented a transfer import command handler that imports a file from a given URL and commits it. --- pom.xml | 8 +- .../TransformImportCommandHandler.java | 59 ++++++++ .../databind/TransformImportOptions.java | 26 ++++ .../transform/service/TransformService.java | 3 + .../service/TransformServiceXsl.java | 130 +++++++++++++++--- 5 files changed, 202 insertions(+), 24 deletions(-) create mode 100644 src/main/java/se/simonsoft/cms/transform/command/TransformImportCommandHandler.java diff --git a/pom.xml b/pom.xml index df0acc4..708a701 100644 --- a/pom.xml +++ b/pom.xml @@ -37,12 +37,6 @@ se.simonsoft cms-reporting 2.2.1 - - - javax.ws.rs - javax.ws.rs-api - - @@ -85,7 +79,7 @@ 3.1.3 test - + diff --git a/src/main/java/se/simonsoft/cms/transform/command/TransformImportCommandHandler.java b/src/main/java/se/simonsoft/cms/transform/command/TransformImportCommandHandler.java new file mode 100644 index 0000000..7547819 --- /dev/null +++ b/src/main/java/se/simonsoft/cms/transform/command/TransformImportCommandHandler.java @@ -0,0 +1,59 @@ +/** + * Copyright (C) 2009-2017 Simonsoft Nordic AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package se.simonsoft.cms.transform.command; + +import javax.inject.Inject; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import se.simonsoft.cms.item.CmsItemId; +import se.simonsoft.cms.item.command.ExternalCommandHandler; +import se.simonsoft.cms.transform.config.databind.TransformImportOptions; +import se.simonsoft.cms.transform.service.TransformService; + +public class TransformImportCommandHandler implements ExternalCommandHandler { + + private final Logger logger = LoggerFactory.getLogger(TransformImportCommandHandler.class); + private final TransformService transformService; + + @Inject + public TransformImportCommandHandler(TransformService transformService) { + this.transformService = transformService; + } + + @Override + public String handleExternalCommand(CmsItemId item, TransformImportOptions arguments) { + + logger.info("Starting import for item: {} from URL: {}", item.getLogicalId(), arguments.getUrl()); + + try { + transformService.importItem(item, arguments); + String successMessage = "Import completed successfully for item: " + item.getLogicalId(); + logger.info(successMessage); + return successMessage; + + } catch (Exception e) { + String errorMessage = "Import failed for item: " + item.getLogicalId() + " - " + e.getMessage(); + logger.error(errorMessage, e); + throw new RuntimeException(errorMessage, e); + } + } + + @Override + public Class getArgumentsClass() { + return TransformImportOptions.class; + } +} diff --git a/src/main/java/se/simonsoft/cms/transform/config/databind/TransformImportOptions.java b/src/main/java/se/simonsoft/cms/transform/config/databind/TransformImportOptions.java index 2119965..655c8d4 100644 --- a/src/main/java/se/simonsoft/cms/transform/config/databind/TransformImportOptions.java +++ b/src/main/java/se/simonsoft/cms/transform/config/databind/TransformImportOptions.java @@ -1,9 +1,25 @@ +/** + * Copyright (C) 2009-2017 Simonsoft Nordic AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package se.simonsoft.cms.transform.config.databind; import java.util.HashMap; import java.util.Map; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import se.simonsoft.cms.item.properties.CmsItemPropertiesMap; @JsonIgnoreProperties(ignoreUnknown = true) // Allow future changes. public class TransformImportOptions { @@ -62,4 +78,14 @@ public Map getProperties() { public void setProperties(Map properties) { this.properties = properties; } + + public CmsItemPropertiesMap getItemPropertiesMap() { + CmsItemPropertiesMap m = new CmsItemPropertiesMap(); + if (getProperties() != null) { + getProperties().forEach((key, value) -> { + m.put(key, value); + }); + } + return m; + } } diff --git a/src/main/java/se/simonsoft/cms/transform/service/TransformService.java b/src/main/java/se/simonsoft/cms/transform/service/TransformService.java index c94c603..4c8a92d 100644 --- a/src/main/java/se/simonsoft/cms/transform/service/TransformService.java +++ b/src/main/java/se/simonsoft/cms/transform/service/TransformService.java @@ -17,8 +17,11 @@ import se.simonsoft.cms.item.CmsItemId; import se.simonsoft.cms.transform.config.databind.TransformConfig; +import se.simonsoft.cms.transform.config.databind.TransformImportOptions; public interface TransformService { void transform(CmsItemId item, TransformConfig config); + + void importItem(CmsItemId item, TransformImportOptions config); } diff --git a/src/main/java/se/simonsoft/cms/transform/service/TransformServiceXsl.java b/src/main/java/se/simonsoft/cms/transform/service/TransformServiceXsl.java index fa337f9..66e53c2 100644 --- a/src/main/java/se/simonsoft/cms/transform/service/TransformServiceXsl.java +++ b/src/main/java/se/simonsoft/cms/transform/service/TransformServiceXsl.java @@ -18,7 +18,9 @@ import java.io.IOException; import java.io.InputStream; import java.io.PushbackInputStream; -import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.HashSet; @@ -55,6 +57,7 @@ import se.simonsoft.cms.item.structure.CmsItemClassificationXml; import se.simonsoft.cms.reporting.CmsItemLookupReporting; import se.simonsoft.cms.transform.config.databind.TransformConfig; +import se.simonsoft.cms.transform.config.databind.TransformImportOptions; import se.simonsoft.cms.transform.lookup.CmsItemLookupTransform; import se.simonsoft.cms.xmlsource.handler.s9api.XmlSourceDocumentS9api; import se.simonsoft.cms.xmlsource.handler.s9api.XmlSourceReaderS9api; @@ -81,7 +84,11 @@ public class TransformServiceXsl implements TransformService { private static final String TRANSFORM_NAME_PROP_KEY = "abx:TransformName"; private static final int HISTORY_MSG_MAX_SIZE = 2000; private static final String OUTPUT_TRANSFORM = "se/simonsoft/cms/transform/output.xsl"; - + + private static final int HTTP_URL_CONNECTION_READ_TIMEOUT = 60000; // 60 seconds + private static final int HTTP_URL_CONNECTION_CONNECT_TIMEOUT = 30000; // 30 seconds + private static final String HTTP_URL_CONNECTION_USER_AGENT = "cms-transform/1.0"; + private static final Logger logger = LoggerFactory.getLogger(TransformServiceXsl.class); @Inject @@ -178,7 +185,71 @@ public void transform(CmsItemId itemId, TransformConfig config) { RepoRevision r = commit.run(patchset); logger.debug("Transform complete, commited with rev: {}", r.getNumber()); } - + + public void importItem(CmsItemId item, TransformImportOptions config) { + if (config == null) { + throw new IllegalArgumentException("Import requires a valid TransformImportOptions object."); + } + + String url = config.getUrl(); + if (url == null || url.trim().isEmpty()) { + throw new IllegalArgumentException("Import requires a valid URL."); + } + + final CmsRepository repository = item.getRepository(); + final RepoRevision baseRevision = repoLookup.getYoungest(repository); + final CmsPatchset patchset = new CmsPatchset(repository, baseRevision); + final boolean overwrite = Boolean.valueOf(config.getParams().get("overwrite")); + final CmsItemPropertiesMap properties = config.getItemPropertiesMap(); + + final Set locked = new HashSet<>(); + + try { + InputStream downloadedStream = download(url); + + CmsItemLock lock = addToPatchset(patchset, item.getRelPath(), downloadedStream, overwrite, properties); + if (lock != null) { + locked.add(lock); + } + + String comment = config.getParams().get("comment"); + if (comment != null && !comment.trim().isEmpty()) { + patchset.setHistoryMessage(comment); + } + + RepoRevision r = commit.run(patchset); + logger.info("Import complete from URL: {}, committed with rev: {}", url, r.getNumber()); + + } catch (IOException | URISyntaxException e) { + logger.error("Failed to download content from URL: {}", url, e); + unlockItemsFailure(locked); + throw new RuntimeException("Failed to download content from URL: " + url, e); + } catch (RuntimeException e) { + logger.warn("Failed to import item: {}", e.getMessage(), e); + unlockItemsFailure(locked); + throw e; + } + } + + private InputStream download(String url) throws IOException, URISyntaxException { + URI uri = new URI(url); + HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection(); + connection.setRequestMethod("GET"); + connection.setConnectTimeout(HTTP_URL_CONNECTION_CONNECT_TIMEOUT); + connection.setReadTimeout(HTTP_URL_CONNECTION_READ_TIMEOUT); + connection.setInstanceFollowRedirects(true); + + // Set user agent to avoid blocking by some servers + connection.setRequestProperty("User-Agent", HTTP_URL_CONNECTION_USER_AGENT); + + int responseCode = connection.getResponseCode(); + if (responseCode == HttpURLConnection.HTTP_OK) { + return connection.getInputStream(); + } else { + throw new IOException("HTTP request failed with response code: " + responseCode + " for URL: " + url); + } + } + private boolean isTransformable(CmsItemId itemId) { CmsItemClassificationXml classification = new CmsItemClassificationXml(); @@ -187,11 +258,8 @@ private boolean isTransformable(CmsItemId itemId) { } CmsItem item = this.itemLookup.getItem(itemId); // TODO: Use cms-item method when available. - if (isCmsClass(item.getProperties(), "tikahtml")) { - return true; - } - return false; - } + return isCmsClass(item.getProperties(), "tikahtml"); + } private Set transformItem(CmsItemId baseItemId, TransformConfig config, TransformerService transformerService, TransformOptions transformOptions, CmsPatchset patchset) { @@ -267,6 +335,40 @@ private String getCompleteMessageString(String comment, List messages) { return sb.toString(); } + private CmsItemLock addToPatchset(CmsPatchset patchset, CmsItemPath relPath, InputStream inputStream, boolean overwrite, CmsItemPropertiesMap properties) { + CmsItemLock lock = null; + try { + final InputStream contentStream = getInputStreamNotEmpty(inputStream); + boolean pathExists = pathExists(patchset.getRepository(), relPath); + if (!pathExists) { + addFolderExists(patchset, relPath.getParent()); + logger.debug("No file at path: '{}' will add new file.", relPath); + FileAdd fileAdd = new FileAdd(relPath, contentStream); + fileAdd.setPropertyChange(properties); + patchset.add(fileAdd); + } else if (overwrite){ + logger.debug("Overwrite is allowed, existing file at path '{}' will be modified.", relPath.getPath()); + CmsItemId itemId = patchset.getRepository().getItemId().withRelPath(relPath); + CmsItemLockCollection locks = commit.lock(TRANSFORM_LOCK_COMMENT, patchset.getBaseRevision(), itemId.getRelPath()); + if (locks != null && locks.getSingle() == null) { + throw new IllegalStateException("Unable to retrieve the lock token after locking " + itemId); + } + lock = locks.getSingle(); + patchset.addLock(lock); + FileModificationLocked fileMod = new FileModificationLocked(relPath, contentStream); + fileMod.setPropertyChange(properties); + patchset.add(fileMod); + } else { + throw new IllegalStateException("Item already exists, config prohibiting overwrite of existing items."); + } + } catch (IOException e) { + throw new RuntimeException("Failed to read stream from import.", e); + } catch (EmptyStreamException e) { + logger.warn("Import of item at path: '{}' resulted in empty document, will be discarded.", relPath); + } + return lock; + } + private CmsItemLock addToPatchset(CmsPatchset patchset, CmsItemPath relPath, TransformStreamProvider streamProvider, boolean overwrite, CmsItemPropertiesMap properties) { CmsItemLock lock = null; try { @@ -374,7 +476,7 @@ private InputStream getInputStreamNotEmpty(InputStream inputStream) throws IOExc } private boolean emptyExceptDeclaration(String data) { - return data.substring(data.indexOf("?>") + 2, data.length()).trim().isEmpty(); + return data.substring(data.indexOf("?>") + 2).trim().isEmpty(); } private CmsItemPropertiesMap getProperties(CmsItemId baseId, TransformConfig config) { @@ -409,13 +511,8 @@ private void addFolderExists(CmsPatchset patchset, CmsItemPath parentPath) { } private String decodeHref(String href) { - try { - href = java.net.URLDecoder.decode(href, "UTF-8"); - } catch (UnsupportedEncodingException e) { - logger.error("Could not decode URL", e); - throw new IllegalArgumentException("Could not decode URL: " + href); - } - return href; + href = java.net.URLDecoder.decode(href, StandardCharsets.UTF_8); + return href; } private static boolean isCmsClass(CmsItemProperties properties, String cmsClass) { @@ -442,5 +539,4 @@ public EmptyStreamException(String message) { } } - } From ad4e424ce9fa1f4305d8cae68529e2175fbe525d Mon Sep 17 00:00:00 2001 From: Omid Manikhi Date: Wed, 27 Aug 2025 10:00:47 +0200 Subject: [PATCH 04/14] Implemented the TransformResource serving the REST functions. --- .../cms/transform/rest/TransformResource.java | 86 +++++++++++++++++++ .../service/TransformServiceXsl.java | 1 + 2 files changed, 87 insertions(+) create mode 100644 src/main/java/se/simonsoft/cms/transform/rest/TransformResource.java diff --git a/src/main/java/se/simonsoft/cms/transform/rest/TransformResource.java b/src/main/java/se/simonsoft/cms/transform/rest/TransformResource.java new file mode 100644 index 0000000..1788f28 --- /dev/null +++ b/src/main/java/se/simonsoft/cms/transform/rest/TransformResource.java @@ -0,0 +1,86 @@ +/** + * Copyright (C) 2009-2017 Simonsoft Nordic AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package se.simonsoft.cms.transform.rest; + +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import se.simonsoft.cms.item.CmsItemId; +import se.simonsoft.cms.item.impl.CmsItemIdArg; +import se.simonsoft.cms.transform.service.TransformService; +import se.simonsoft.cms.transform.config.databind.TransformImportOptions; + +@Path("/transform5") +public class TransformResource { + + private final Logger logger = LoggerFactory.getLogger(TransformResource.class); + private final TransformService transformService; + + @Inject + public TransformResource(TransformService transformService) { + this.transformService = transformService; + } + + @POST + @Path("/api/import") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Response importItem(@QueryParam("itemId") String itemIdParam, TransformImportOptions importOptions) { + + if (itemIdParam == null || itemIdParam.trim().isEmpty()) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("{\"error\": \"itemId query parameter is required\"}") + .build(); + } + + if (importOptions == null) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("{\"error\": \"Import options must be provided in request body\"}") + .build(); + } + + try { + CmsItemId itemId = new CmsItemIdArg(itemIdParam); + transformService.importItem(itemId, importOptions); + + String successMessage = "Import completed successfully for item: " + itemId.getLogicalId(); + return Response.ok() + .entity("{\"message\": \"" + successMessage + "\"}") + .build(); + + } catch (IllegalArgumentException e) { + logger.warn("Invalid itemId parameter: {}", itemIdParam, e); + return Response.status(Response.Status.BAD_REQUEST) + .entity("{\"error\": \"Invalid itemId parameter: " + e.getMessage() + "\"}") + .build(); + + } catch (Exception e) { + logger.error("Import failed for itemId: {}", itemIdParam, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity("{\"error\": \"Import failed: " + e.getMessage() + "\"}") + .build(); + } + } +} diff --git a/src/main/java/se/simonsoft/cms/transform/service/TransformServiceXsl.java b/src/main/java/se/simonsoft/cms/transform/service/TransformServiceXsl.java index 66e53c2..d44821a 100644 --- a/src/main/java/se/simonsoft/cms/transform/service/TransformServiceXsl.java +++ b/src/main/java/se/simonsoft/cms/transform/service/TransformServiceXsl.java @@ -186,6 +186,7 @@ public void transform(CmsItemId itemId, TransformConfig config) { logger.debug("Transform complete, commited with rev: {}", r.getNumber()); } + @Override public void importItem(CmsItemId item, TransformImportOptions config) { if (config == null) { throw new IllegalArgumentException("Import requires a valid TransformImportOptions object."); From 970d3f1b150b7c9712f5db6a0d4796f9eafc7a8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20=C3=85kesson?= Date: Wed, 27 Aug 2025 11:13:33 +0200 Subject: [PATCH 05/14] Needed for Java 17 compatibility. --- pom.xml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pom.xml b/pom.xml index 708a701..6c5bb90 100644 --- a/pom.xml +++ b/pom.xml @@ -81,5 +81,21 @@ + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${surefire-plugin.version} + + + --add-opens java.base/java.lang=ALL-UNNAMED + + + + + From 2d3b5a1b0e8d90fa86152167dc896a89abadf56b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20=C3=85kesson?= Date: Wed, 27 Aug 2025 11:20:34 +0200 Subject: [PATCH 06/14] TODO: Consider supporting revision properties (backend recently supports). --- .../cms/transform/config/databind/TransformImportOptions.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/se/simonsoft/cms/transform/config/databind/TransformImportOptions.java b/src/main/java/se/simonsoft/cms/transform/config/databind/TransformImportOptions.java index 655c8d4..950924a 100644 --- a/src/main/java/se/simonsoft/cms/transform/config/databind/TransformImportOptions.java +++ b/src/main/java/se/simonsoft/cms/transform/config/databind/TransformImportOptions.java @@ -52,6 +52,7 @@ public class TransformImportOptions { private String url; // Typically an http / https url, no authentication required. Redirects must be followed. private String content; // Content to import, typically XML or JSON. private Map properties = new HashMap<>(); // Properties to set on the item. + private Map revprops = new HashMap<>(); // TODO: Consider supporting revision properties (backend recently supports). public Map getParams() { From f028cf14eeb869baf9905e2c45f98100aa9baedc Mon Sep 17 00:00:00 2001 From: Omid Manikhi Date: Thu, 28 Aug 2025 11:06:36 +0200 Subject: [PATCH 07/14] Fixed the injection issues. --- .../command/TransformImportCommandHandler.java | 11 +++++++---- .../cms/transform/rest/TransformResource.java | 15 +++++++++------ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/main/java/se/simonsoft/cms/transform/command/TransformImportCommandHandler.java b/src/main/java/se/simonsoft/cms/transform/command/TransformImportCommandHandler.java index 7547819..2c35c95 100644 --- a/src/main/java/se/simonsoft/cms/transform/command/TransformImportCommandHandler.java +++ b/src/main/java/se/simonsoft/cms/transform/command/TransformImportCommandHandler.java @@ -20,18 +20,21 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import se.simonsoft.cms.item.CmsItemId; +import se.simonsoft.cms.item.CmsRepository; import se.simonsoft.cms.item.command.ExternalCommandHandler; import se.simonsoft.cms.transform.config.databind.TransformImportOptions; import se.simonsoft.cms.transform.service.TransformService; +import java.util.Map; + public class TransformImportCommandHandler implements ExternalCommandHandler { private final Logger logger = LoggerFactory.getLogger(TransformImportCommandHandler.class); - private final TransformService transformService; + private final Map transformServiceMap; @Inject - public TransformImportCommandHandler(TransformService transformService) { - this.transformService = transformService; + public TransformImportCommandHandler(Map transformServiceMap) { + this.transformServiceMap = transformServiceMap; } @Override @@ -40,7 +43,7 @@ public String handleExternalCommand(CmsItemId item, TransformImportOptions argum logger.info("Starting import for item: {} from URL: {}", item.getLogicalId(), arguments.getUrl()); try { - transformService.importItem(item, arguments); + transformServiceMap.get(item.getRepository()).importItem(item, arguments); String successMessage = "Import completed successfully for item: " + item.getLogicalId(); logger.info(successMessage); return successMessage; diff --git a/src/main/java/se/simonsoft/cms/transform/rest/TransformResource.java b/src/main/java/se/simonsoft/cms/transform/rest/TransformResource.java index 1788f28..8359b72 100644 --- a/src/main/java/se/simonsoft/cms/transform/rest/TransformResource.java +++ b/src/main/java/se/simonsoft/cms/transform/rest/TransformResource.java @@ -28,23 +28,26 @@ import org.slf4j.LoggerFactory; import se.simonsoft.cms.item.CmsItemId; +import se.simonsoft.cms.item.CmsRepository; import se.simonsoft.cms.item.impl.CmsItemIdArg; import se.simonsoft.cms.transform.service.TransformService; import se.simonsoft.cms.transform.config.databind.TransformImportOptions; +import java.util.Map; + @Path("/transform5") public class TransformResource { private final Logger logger = LoggerFactory.getLogger(TransformResource.class); - private final TransformService transformService; + private final Map transformServiceMap; @Inject - public TransformResource(TransformService transformService) { - this.transformService = transformService; + public TransformResource(Map transformServiceMap) { + this.transformServiceMap = transformServiceMap; } @POST - @Path("/api/import") + @Path("api/import") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public Response importItem(@QueryParam("itemId") String itemIdParam, TransformImportOptions importOptions) { @@ -63,8 +66,8 @@ public Response importItem(@QueryParam("itemId") String itemIdParam, TransformIm try { CmsItemId itemId = new CmsItemIdArg(itemIdParam); - transformService.importItem(itemId, importOptions); - + transformServiceMap.get(itemId.getRepository()).importItem(itemId, importOptions); + String successMessage = "Import completed successfully for item: " + itemId.getLogicalId(); return Response.ok() .entity("{\"message\": \"" + successMessage + "\"}") From 76333995e4366707b0dd8fd151c48cc61c92b0bb Mon Sep 17 00:00:00 2001 From: Omid Manikhi Date: Thu, 28 Aug 2025 12:19:44 +0200 Subject: [PATCH 08/14] Fixed the JSON body deserialization issue. --- .../cms/transform/rest/TransformResource.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/main/java/se/simonsoft/cms/transform/rest/TransformResource.java b/src/main/java/se/simonsoft/cms/transform/rest/TransformResource.java index 8359b72..be543f1 100644 --- a/src/main/java/se/simonsoft/cms/transform/rest/TransformResource.java +++ b/src/main/java/se/simonsoft/cms/transform/rest/TransformResource.java @@ -24,6 +24,7 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -50,7 +51,7 @@ public TransformResource(Map transformServiceMa @Path("api/import") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - public Response importItem(@QueryParam("itemId") String itemIdParam, TransformImportOptions importOptions) { + public Response importItem(@QueryParam("itemId") String itemIdParam, String body) { if (itemIdParam == null || itemIdParam.trim().isEmpty()) { return Response.status(Response.Status.BAD_REQUEST) @@ -58,6 +59,18 @@ public Response importItem(@QueryParam("itemId") String itemIdParam, TransformIm .build(); } + TransformImportOptions importOptions; + + try { + ObjectMapper mapper = new ObjectMapper(); + importOptions = mapper.readValue(body, TransformImportOptions.class); + } catch (Exception e) { + logger.warn("Failed to parse JSON request body", e); + return Response.status(Response.Status.BAD_REQUEST) + .entity("{\"error\": \"Invalid JSON in request body: " + e.getMessage() + "\"}") + .build(); + } + if (importOptions == null) { return Response.status(Response.Status.BAD_REQUEST) .entity("{\"error\": \"Import options must be provided in request body\"}") From 8c22cf24528146e667d181b0488b85d60ed769fa Mon Sep 17 00:00:00 2001 From: Omid Manikhi Date: Tue, 16 Sep 2025 12:11:03 +0200 Subject: [PATCH 09/14] The importItem now supports folder items and auto-naming of the downloaded files. --- .../service/TransformServiceXsl.java | 170 ++++++++++++++++-- 1 file changed, 155 insertions(+), 15 deletions(-) diff --git a/src/main/java/se/simonsoft/cms/transform/service/TransformServiceXsl.java b/src/main/java/se/simonsoft/cms/transform/service/TransformServiceXsl.java index d44821a..71558d3 100644 --- a/src/main/java/se/simonsoft/cms/transform/service/TransformServiceXsl.java +++ b/src/main/java/se/simonsoft/cms/transform/service/TransformServiceXsl.java @@ -19,8 +19,8 @@ import java.io.InputStream; import java.io.PushbackInputStream; import java.net.HttpURLConnection; -import java.net.URI; import java.net.URISyntaxException; +import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.HashSet; @@ -35,6 +35,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.slf4j.helpers.MessageFormatter; import se.simonsoft.cms.item.CmsItem; import se.simonsoft.cms.item.CmsItemId; import se.simonsoft.cms.item.CmsItemKind; @@ -52,6 +53,9 @@ import se.simonsoft.cms.item.info.CmsItemLookup; import se.simonsoft.cms.item.info.CmsItemNotFoundException; import se.simonsoft.cms.item.info.CmsRepositoryLookup; +import se.simonsoft.cms.item.naming.CmsItemNamePattern; +import se.simonsoft.cms.item.naming.CmsItemNaming; +import se.simonsoft.cms.item.naming.CmsItemNamingShard1K; import se.simonsoft.cms.item.properties.CmsItemProperties; import se.simonsoft.cms.item.properties.CmsItemPropertiesMap; import se.simonsoft.cms.item.structure.CmsItemClassificationXml; @@ -89,6 +93,9 @@ public class TransformServiceXsl implements TransformService { private static final int HTTP_URL_CONNECTION_CONNECT_TIMEOUT = 30000; // 30 seconds private static final String HTTP_URL_CONNECTION_USER_AGENT = "cms-transform/1.0"; + private static final String CMS_CLASS_SHARDPARENT = "shardparent"; + private static final String PROPNAME_CONFIG_ITEMNAMEPATTERN = "cmsconfig:ItemNamePattern"; + private static final Logger logger = LoggerFactory.getLogger(TransformServiceXsl.class); @Inject @@ -206,9 +213,8 @@ public void importItem(CmsItemId item, TransformImportOptions config) { final Set locked = new HashSet<>(); try { - InputStream downloadedStream = download(url); - - CmsItemLock lock = addToPatchset(patchset, item.getRelPath(), downloadedStream, overwrite, properties); + DownloadResult downloadResult = download(url); + CmsItemLock lock = addToPatchset(patchset, item, downloadResult, overwrite, properties); if (lock != null) { locked.add(lock); } @@ -232,9 +238,8 @@ public void importItem(CmsItemId item, TransformImportOptions config) { } } - private InputStream download(String url) throws IOException, URISyntaxException { - URI uri = new URI(url); - HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection(); + private DownloadResult download(String url) throws IOException, URISyntaxException { + HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); connection.setRequestMethod("GET"); connection.setConnectTimeout(HTTP_URL_CONNECTION_CONNECT_TIMEOUT); connection.setReadTimeout(HTTP_URL_CONNECTION_READ_TIMEOUT); @@ -245,12 +250,68 @@ private InputStream download(String url) throws IOException, URISyntaxException int responseCode = connection.getResponseCode(); if (responseCode == HttpURLConnection.HTTP_OK) { - return connection.getInputStream(); + String contentType = connection.getContentType(); + String extension = getExtensionFromContentType(contentType); + return new DownloadResult(connection.getInputStream(), contentType, extension); } else { throw new IOException("HTTP request failed with response code: " + responseCode + " for URL: " + url); } } + private String getExtensionFromContentType(String contentType) { + if (contentType == null) return null; + String mimeType = contentType.toLowerCase().split(";")[0].trim(); + switch (mimeType) { + // Images + case "image/jpeg": return "jpg"; + case "image/png": return "png"; + case "image/gif": return "gif"; + case "image/svg+xml": return "svg"; + case "image/webp": return "webp"; + case "image/bmp": return "bmp"; + case "image/tiff": return "tiff"; + case "image/x-icon": return "ico"; + + // Text/Markup + case "text/xml": + case "application/xml": return "xml"; + case "application/json": return "json"; + case "text/plain": return "txt"; + case "text/html": return "html"; + case "text/css": return "css"; + case "text/csv": return "csv"; + + // Documents + case "application/pdf": return "pdf"; + case "application/msword": return "doc"; + case "application/vnd.openxmlformats-officedocument.wordprocessingml.document": return "docx"; + case "application/vnd.ms-excel": return "xls"; + case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": return "xlsx"; + case "application/vnd.ms-powerpoint": return "ppt"; + case "application/vnd.openxmlformats-officedocument.presentationml.presentation": return "pptx"; + case "application/rtf": return "rtf"; + + // Archives + case "application/zip": return "zip"; + case "application/x-rar-compressed": return "rar"; + case "application/x-tar": return "tar"; + case "application/gzip": return "gz"; + + // Code/Scripts + case "application/javascript": + case "text/javascript": return "js"; + + // Audio/Video + case "audio/mpeg": return "mp3"; + case "audio/wav": return "wav"; + case "video/mp4": return "mp4"; + case "video/mpeg": return "mpeg"; + case "video/quicktime": return "mov"; + + default: return null; + } + } + private boolean isTransformable(CmsItemId itemId) { CmsItemClassificationXml classification = new CmsItemClassificationXml(); @@ -336,20 +397,82 @@ private String getCompleteMessageString(String comment, List messages) { return sb.toString(); } - private CmsItemLock addToPatchset(CmsPatchset patchset, CmsItemPath relPath, InputStream inputStream, boolean overwrite, CmsItemPropertiesMap properties) { + private CmsItemLock addToPatchset(CmsPatchset patchset, CmsItemId itemId, DownloadResult downloadResult, boolean overwrite, CmsItemPropertiesMap properties) { + boolean pathExists; + boolean isFolder = false; CmsItemLock lock = null; + CmsItemPath relPath = itemId.getRelPath(); + CmsRepository repository = itemId.getRepository(); + InputStream inputStream = downloadResult.getInputStream(); + CmsItemNaming itemNaming = new CmsItemNamingShard1K(repository, itemLookup); + // Determine whether the given path exists and if it is a folder + try { + CmsItem item = itemLookup.getItem(itemId); + isFolder = item.getKind().isFolder(); + pathExists = true; + } catch (CmsItemNotFoundException e) { + pathExists = false; + } try { final InputStream contentStream = getInputStreamNotEmpty(inputStream); - boolean pathExists = pathExists(patchset.getRepository(), relPath); if (!pathExists) { - addFolderExists(patchset, relPath.getParent()); - logger.debug("No file at path: '{}' will add new file.", relPath); - FileAdd fileAdd = new FileAdd(relPath, contentStream); + CmsItemPath itemPath = relPath; + // The path doesn't exist, it could still be either a file or a folder + isFolder = relPath.getName().endsWith("/") || !relPath.getName().contains("."); + if (isFolder) { + // It's a folder + addFolderExists(patchset, relPath); + // Auto-name the file + CmsItem location = itemLookup.getItem(repository.getItemId(relPath, null)); + CmsItemProperties locationProps = location.getProperties(); + if (!isCmsClass(locationProps, CMS_CLASS_SHARDPARENT)) { + String msg = MessageFormatter.format("Location is not a shardparent: {}", relPath).getMessage(); + logger.error(msg); + throw new IllegalArgumentException(msg); + } + // The cmsconfig name pattern overrides the bursting rule. + if (!locationProps.containsProperty(PROPNAME_CONFIG_ITEMNAMEPATTERN)) { + String msg = MessageFormatter.format("Location does not define a name pattern: {}", relPath).getMessage(); + logger.error(msg); + throw new IllegalArgumentException(msg); + } + CmsItemNamePattern namePattern = new CmsItemNamePattern(locationProps.getString(PROPNAME_CONFIG_ITEMNAMEPATTERN)); + itemPath = itemNaming.getItemPath(relPath, namePattern, downloadResult.getExtension()); + } else { + // It's a file + addFolderExists(patchset, relPath.getParent()); + } + logger.debug("No file at path: '{}' will add new file.", itemPath); + FileAdd fileAdd = new FileAdd(itemPath, contentStream); fileAdd.setPropertyChange(properties); patchset.add(fileAdd); - } else if (overwrite){ + } else if (isFolder) { + // The path exists and is a folder + addFolderExists(patchset, relPath); + // Auto-name the file + CmsItem location = itemLookup.getItem(repository.getItemId(relPath, null)); + CmsItemProperties locationProps = location.getProperties(); + if (!isCmsClass(locationProps, CMS_CLASS_SHARDPARENT)) { + String msg = MessageFormatter.format("Location is not a shardparent: {}", relPath).getMessage(); + logger.error(msg); + throw new IllegalArgumentException(msg); + } + // The cmsconfig name pattern overrides the bursting rule. + if (!locationProps.containsProperty(PROPNAME_CONFIG_ITEMNAMEPATTERN)) { + String msg = MessageFormatter.format("Location does not define a name pattern: {}", relPath).getMessage(); + logger.error(msg); + throw new IllegalArgumentException(msg); + } + CmsItemNamePattern namePattern = new CmsItemNamePattern(locationProps.getString(PROPNAME_CONFIG_ITEMNAMEPATTERN)); + CmsItemPath itemPath = itemNaming.getItemPath(relPath, namePattern, "jpg"); + logger.debug("No file at path: '{}' will add new file.", itemPath); + addFolderExists(patchset, itemPath.getParent()); + FileAdd fileAdd = new FileAdd(itemPath, contentStream); + fileAdd.setPropertyChange(properties); + patchset.add(fileAdd); + } else if (overwrite) { + // The file exists and is, and we are allowed to overwrite logger.debug("Overwrite is allowed, existing file at path '{}' will be modified.", relPath.getPath()); - CmsItemId itemId = patchset.getRepository().getItemId().withRelPath(relPath); CmsItemLockCollection locks = commit.lock(TRANSFORM_LOCK_COMMENT, patchset.getBaseRevision(), itemId.getRelPath()); if (locks != null && locks.getSingle() == null) { throw new IllegalStateException("Unable to retrieve the lock token after locking " + itemId); @@ -360,6 +483,7 @@ private CmsItemLock addToPatchset(CmsPatchset patchset, CmsItemPath relPath, Inp fileMod.setPropertyChange(properties); patchset.add(fileMod); } else { + // The file exists, and we are not allowed to overwrite it throw new IllegalStateException("Item already exists, config prohibiting overwrite of existing items."); } } catch (IOException e) { @@ -540,4 +664,20 @@ public EmptyStreamException(String message) { } } + + private class DownloadResult { + private final InputStream inputStream; + private final String contentType; + private final String extension; + + public DownloadResult(InputStream inputStream, String contentType, String extension) { + this.inputStream = inputStream; + this.contentType = contentType; + this.extension = extension; + } + + public InputStream getInputStream() { return inputStream; } + public String getContentType() { return contentType; } + public String getExtension() { return extension; } + } } From cb1c916a3fa6b54e79d6f05e62a43775da9ac643 Mon Sep 17 00:00:00 2001 From: Omid Manikhi Date: Mon, 22 Sep 2025 09:57:07 +0200 Subject: [PATCH 10/14] Addressed the review comments. --- .../cms/transform/rest/TransformResource.java | 27 +++---- .../service/TransformServiceXsl.java | 70 +++++++------------ 2 files changed, 36 insertions(+), 61 deletions(-) diff --git a/src/main/java/se/simonsoft/cms/transform/rest/TransformResource.java b/src/main/java/se/simonsoft/cms/transform/rest/TransformResource.java index be543f1..7dd48ea 100644 --- a/src/main/java/se/simonsoft/cms/transform/rest/TransformResource.java +++ b/src/main/java/se/simonsoft/cms/transform/rest/TransformResource.java @@ -24,6 +24,7 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -51,12 +52,10 @@ public TransformResource(Map transformServiceMa @Path("api/import") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - public Response importItem(@QueryParam("itemId") String itemIdParam, String body) { + public Response importItem(@QueryParam("item") CmsItemIdArg itemId, String body) { - if (itemIdParam == null || itemIdParam.trim().isEmpty()) { - return Response.status(Response.Status.BAD_REQUEST) - .entity("{\"error\": \"itemId query parameter is required\"}") - .build(); + if (itemId == null) { + throw new IllegalArgumentException("Field 'item': required"); } TransformImportOptions importOptions; @@ -64,11 +63,9 @@ public Response importItem(@QueryParam("itemId") String itemIdParam, String body try { ObjectMapper mapper = new ObjectMapper(); importOptions = mapper.readValue(body, TransformImportOptions.class); - } catch (Exception e) { - logger.warn("Failed to parse JSON request body", e); - return Response.status(Response.Status.BAD_REQUEST) - .entity("{\"error\": \"Invalid JSON in request body: " + e.getMessage() + "\"}") - .build(); + } catch (JsonProcessingException e) { + logger.error("API request with invalid JSON body: {}", body, e); + throw new IllegalArgumentException("Failed to parse request body: " + e.getMessage(), e); } if (importOptions == null) { @@ -78,7 +75,6 @@ public Response importItem(@QueryParam("itemId") String itemIdParam, String body } try { - CmsItemId itemId = new CmsItemIdArg(itemIdParam); transformServiceMap.get(itemId.getRepository()).importItem(itemId, importOptions); String successMessage = "Import completed successfully for item: " + itemId.getLogicalId(); @@ -87,13 +83,10 @@ public Response importItem(@QueryParam("itemId") String itemIdParam, String body .build(); } catch (IllegalArgumentException e) { - logger.warn("Invalid itemId parameter: {}", itemIdParam, e); - return Response.status(Response.Status.BAD_REQUEST) - .entity("{\"error\": \"Invalid itemId parameter: " + e.getMessage() + "\"}") - .build(); - + logger.warn("Invalid itemId parameter: {}", itemId.getLogicalId(), e); + throw e; } catch (Exception e) { - logger.error("Import failed for itemId: {}", itemIdParam, e); + logger.error("Import failed for itemId: {}", itemId.getLogicalId(), e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity("{\"error\": \"Import failed: " + e.getMessage() + "\"}") .build(); diff --git a/src/main/java/se/simonsoft/cms/transform/service/TransformServiceXsl.java b/src/main/java/se/simonsoft/cms/transform/service/TransformServiceXsl.java index 71558d3..3cf0da3 100644 --- a/src/main/java/se/simonsoft/cms/transform/service/TransformServiceXsl.java +++ b/src/main/java/se/simonsoft/cms/transform/service/TransformServiceXsl.java @@ -18,10 +18,13 @@ import java.io.IOException; import java.io.InputStream; import java.io.PushbackInputStream; -import java.net.HttpURLConnection; +import java.net.URI; import java.net.URISyntaxException; -import java.net.URL; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.util.Arrays; import java.util.HashSet; import java.util.Iterator; @@ -227,7 +230,7 @@ public void importItem(CmsItemId item, TransformImportOptions config) { RepoRevision r = commit.run(patchset); logger.info("Import complete from URL: {}, committed with rev: {}", url, r.getNumber()); - } catch (IOException | URISyntaxException e) { + } catch (IOException | URISyntaxException | InterruptedException e) { logger.error("Failed to download content from URL: {}", url, e); unlockItemsFailure(locked); throw new RuntimeException("Failed to download content from URL: " + url, e); @@ -238,23 +241,27 @@ public void importItem(CmsItemId item, TransformImportOptions config) { } } - private DownloadResult download(String url) throws IOException, URISyntaxException { - HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); - connection.setRequestMethod("GET"); - connection.setConnectTimeout(HTTP_URL_CONNECTION_CONNECT_TIMEOUT); - connection.setReadTimeout(HTTP_URL_CONNECTION_READ_TIMEOUT); - connection.setInstanceFollowRedirects(true); + private DownloadResult download(String url) throws IOException, URISyntaxException, InterruptedException { + HttpClient client = HttpClient.newBuilder() + .connectTimeout(Duration.ofMillis(HTTP_URL_CONNECTION_CONNECT_TIMEOUT)) + .followRedirects(HttpClient.Redirect.NORMAL) + .build(); - // Set user agent to avoid blocking by some servers - connection.setRequestProperty("User-Agent", HTTP_URL_CONNECTION_USER_AGENT); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .timeout(Duration.ofMillis(HTTP_URL_CONNECTION_READ_TIMEOUT)) + .header("User-Agent", HTTP_URL_CONNECTION_USER_AGENT) + .GET() + .build(); - int responseCode = connection.getResponseCode(); - if (responseCode == HttpURLConnection.HTTP_OK) { - String contentType = connection.getContentType(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofInputStream()); + + if (response.statusCode() == 200) { + String contentType = response.headers().firstValue("Content-Type").orElse(null); String extension = getExtensionFromContentType(contentType); - return new DownloadResult(connection.getInputStream(), contentType, extension); + return new DownloadResult(response.body(), contentType, extension); } else { - throw new IOException("HTTP request failed with response code: " + responseCode + " for URL: " + url); + throw new IOException("HTTP request failed with response code: " + response.statusCode() + " for URL: " + url); } } @@ -416,34 +423,9 @@ private CmsItemLock addToPatchset(CmsPatchset patchset, CmsItemId itemId, Downlo try { final InputStream contentStream = getInputStreamNotEmpty(inputStream); if (!pathExists) { - CmsItemPath itemPath = relPath; - // The path doesn't exist, it could still be either a file or a folder - isFolder = relPath.getName().endsWith("/") || !relPath.getName().contains("."); - if (isFolder) { - // It's a folder - addFolderExists(patchset, relPath); - // Auto-name the file - CmsItem location = itemLookup.getItem(repository.getItemId(relPath, null)); - CmsItemProperties locationProps = location.getProperties(); - if (!isCmsClass(locationProps, CMS_CLASS_SHARDPARENT)) { - String msg = MessageFormatter.format("Location is not a shardparent: {}", relPath).getMessage(); - logger.error(msg); - throw new IllegalArgumentException(msg); - } - // The cmsconfig name pattern overrides the bursting rule. - if (!locationProps.containsProperty(PROPNAME_CONFIG_ITEMNAMEPATTERN)) { - String msg = MessageFormatter.format("Location does not define a name pattern: {}", relPath).getMessage(); - logger.error(msg); - throw new IllegalArgumentException(msg); - } - CmsItemNamePattern namePattern = new CmsItemNamePattern(locationProps.getString(PROPNAME_CONFIG_ITEMNAMEPATTERN)); - itemPath = itemNaming.getItemPath(relPath, namePattern, downloadResult.getExtension()); - } else { - // It's a file - addFolderExists(patchset, relPath.getParent()); - } - logger.debug("No file at path: '{}' will add new file.", itemPath); - FileAdd fileAdd = new FileAdd(itemPath, contentStream); + addFolderExists(patchset, relPath.getParent()); + logger.debug("No file at path: '{}' will add new file.", relPath); + FileAdd fileAdd = new FileAdd(relPath, contentStream); fileAdd.setPropertyChange(properties); patchset.add(fileAdd); } else if (isFolder) { From 45adf8ae00091814f3694c0e9ff7a7b09fb7b4a7 Mon Sep 17 00:00:00 2001 From: Omid Manikhi Date: Tue, 23 Sep 2025 10:02:39 +0200 Subject: [PATCH 11/14] Uncommented User-Agent and improved the itemId logging. --- .../se/simonsoft/cms/transform/rest/TransformResource.java | 4 ++-- .../simonsoft/cms/transform/service/TransformServiceXsl.java | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/se/simonsoft/cms/transform/rest/TransformResource.java b/src/main/java/se/simonsoft/cms/transform/rest/TransformResource.java index 7dd48ea..f20e9c9 100644 --- a/src/main/java/se/simonsoft/cms/transform/rest/TransformResource.java +++ b/src/main/java/se/simonsoft/cms/transform/rest/TransformResource.java @@ -83,10 +83,10 @@ public Response importItem(@QueryParam("item") CmsItemIdArg itemId, String body) .build(); } catch (IllegalArgumentException e) { - logger.warn("Invalid itemId parameter: {}", itemId.getLogicalId(), e); + logger.warn("Invalid itemId parameter: {}", itemId, e); throw e; } catch (Exception e) { - logger.error("Import failed for itemId: {}", itemId.getLogicalId(), e); + logger.error("Import failed for itemId: {}", itemId, e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity("{\"error\": \"Import failed: " + e.getMessage() + "\"}") .build(); diff --git a/src/main/java/se/simonsoft/cms/transform/service/TransformServiceXsl.java b/src/main/java/se/simonsoft/cms/transform/service/TransformServiceXsl.java index 3cf0da3..5b59305 100644 --- a/src/main/java/se/simonsoft/cms/transform/service/TransformServiceXsl.java +++ b/src/main/java/se/simonsoft/cms/transform/service/TransformServiceXsl.java @@ -250,7 +250,8 @@ private DownloadResult download(String url) throws IOException, URISyntaxExcepti HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .timeout(Duration.ofMillis(HTTP_URL_CONNECTION_READ_TIMEOUT)) - .header("User-Agent", HTTP_URL_CONNECTION_USER_AGENT) + // Use the default User-Agent for now. Consider injecting a good universal value in CMS 6.0. + // .header("User-Agent", HTTP_URL_CONNECTION_USER_AGENT) .GET() .build(); From c729f28e2fb88ba1b98744e83ca20332dfdd541e Mon Sep 17 00:00:00 2001 From: Omid Manikhi Date: Tue, 21 Oct 2025 09:38:05 +0200 Subject: [PATCH 12/14] Incorporated pathnamebase and pathext parameters and implemented support for importing content alongside url. --- .../cms/transform/rest/TransformResource.java | 39 ++- .../transform/service/TransformService.java | 4 +- .../service/TransformServiceXsl.java | 244 ++++++------------ 3 files changed, 102 insertions(+), 185 deletions(-) diff --git a/src/main/java/se/simonsoft/cms/transform/rest/TransformResource.java b/src/main/java/se/simonsoft/cms/transform/rest/TransformResource.java index f20e9c9..4b5f749 100644 --- a/src/main/java/se/simonsoft/cms/transform/rest/TransformResource.java +++ b/src/main/java/se/simonsoft/cms/transform/rest/TransformResource.java @@ -26,6 +26,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,24 +36,30 @@ import se.simonsoft.cms.transform.service.TransformService; import se.simonsoft.cms.transform.config.databind.TransformImportOptions; +import java.util.HashMap; import java.util.Map; +import java.util.Set; @Path("/transform5") public class TransformResource { private final Logger logger = LoggerFactory.getLogger(TransformResource.class); private final Map transformServiceMap; + private final ObjectWriter objectWriter; + + private static final int MAX_CONTENT_SIZE_MB = 5; @Inject - public TransformResource(Map transformServiceMap) { + public TransformResource(Map transformServiceMap, ObjectWriter objectWriter) { this.transformServiceMap = transformServiceMap; + this.objectWriter = objectWriter; } @POST @Path("api/import") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - public Response importItem(@QueryParam("item") CmsItemIdArg itemId, String body) { + public Response importItem(@QueryParam("item") CmsItemIdArg itemId, String body) throws JsonProcessingException { if (itemId == null) { throw new IllegalArgumentException("Field 'item': required"); @@ -74,22 +81,26 @@ public Response importItem(@QueryParam("item") CmsItemIdArg itemId, String body) .build(); } - try { - transformServiceMap.get(itemId.getRepository()).importItem(itemId, importOptions); + String url = importOptions.getUrl(); + String content = importOptions.getContent(); - String successMessage = "Import completed successfully for item: " + itemId.getLogicalId(); - return Response.ok() - .entity("{\"message\": \"" + successMessage + "\"}") - .build(); - + if ((content != null && url != null) || (content == null && url == null)) { + throw new IllegalArgumentException("Import requires either a valid URL or content."); + } else if (content != null && content.length() > MAX_CONTENT_SIZE_MB * 1024 * 1024) { + throw new IllegalArgumentException(String.format("Largest allowed content size is %d MBs.", MAX_CONTENT_SIZE_MB)); + } + + try { + Map> response = new HashMap<>(); + Set items = transformServiceMap.get(itemId.getRepository()).importItem(itemId, importOptions); + response.put("items", items); + return Response.ok().entity(objectWriter.writeValueAsString(response)).build(); } catch (IllegalArgumentException e) { - logger.warn("Invalid itemId parameter: {}", itemId, e); + logger.warn("Invalid input parameters for itemId: {}, importOptions: {}", itemId, body, e); throw e; } catch (Exception e) { - logger.error("Import failed for itemId: {}", itemId, e); - return Response.status(Response.Status.INTERNAL_SERVER_ERROR) - .entity("{\"error\": \"Import failed: " + e.getMessage() + "\"}") - .build(); + logger.error("Import failed for itemId: {}, error: {}", itemId, e.getMessage()); + throw e; } } } diff --git a/src/main/java/se/simonsoft/cms/transform/service/TransformService.java b/src/main/java/se/simonsoft/cms/transform/service/TransformService.java index 4c8a92d..a1300b8 100644 --- a/src/main/java/se/simonsoft/cms/transform/service/TransformService.java +++ b/src/main/java/se/simonsoft/cms/transform/service/TransformService.java @@ -19,9 +19,11 @@ import se.simonsoft.cms.transform.config.databind.TransformConfig; import se.simonsoft.cms.transform.config.databind.TransformImportOptions; +import java.util.Set; + public interface TransformService { void transform(CmsItemId item, TransformConfig config); - void importItem(CmsItemId item, TransformImportOptions config); + Set importItem(CmsItemId item, TransformImportOptions config); } diff --git a/src/main/java/se/simonsoft/cms/transform/service/TransformServiceXsl.java b/src/main/java/se/simonsoft/cms/transform/service/TransformServiceXsl.java index 5b59305..b1c74a6 100644 --- a/src/main/java/se/simonsoft/cms/transform/service/TransformServiceXsl.java +++ b/src/main/java/se/simonsoft/cms/transform/service/TransformServiceXsl.java @@ -15,6 +15,7 @@ */ package se.simonsoft.cms.transform.service; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.PushbackInputStream; @@ -38,7 +39,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.slf4j.helpers.MessageFormatter; import se.simonsoft.cms.item.CmsItem; import se.simonsoft.cms.item.CmsItemId; import se.simonsoft.cms.item.CmsItemKind; @@ -53,6 +53,7 @@ import se.simonsoft.cms.item.commit.FileAdd; import se.simonsoft.cms.item.commit.FileModificationLocked; import se.simonsoft.cms.item.commit.FolderExist; +import se.simonsoft.cms.item.impl.CmsItemIdArg; import se.simonsoft.cms.item.info.CmsItemLookup; import se.simonsoft.cms.item.info.CmsItemNotFoundException; import se.simonsoft.cms.item.info.CmsRepositoryLookup; @@ -78,7 +79,6 @@ public class TransformServiceXsl implements TransformService { private final CmsCommit commit; private final CmsItemLookup itemLookup; - @SuppressWarnings("unused") private final CmsItemLookupReporting itemLookupReporting; private final CmsItemLookup itemLookupTransform; private final TransformerServiceFactory transformerServiceFactory; @@ -197,39 +197,83 @@ public void transform(CmsItemId itemId, TransformConfig config) { } @Override - public void importItem(CmsItemId item, TransformImportOptions config) { + public Set importItem(CmsItemId item, TransformImportOptions config) { + Set response = new HashSet<>(); + if (config == null) { throw new IllegalArgumentException("Import requires a valid TransformImportOptions object."); } - String url = config.getUrl(); - if (url == null || url.trim().isEmpty()) { - throw new IllegalArgumentException("Import requires a valid URL."); - } + CmsItemPath relPath = item.getRelPath(); + final String url = config.getUrl(); + final String content = config.getContent(); final CmsRepository repository = item.getRepository(); final RepoRevision baseRevision = repoLookup.getYoungest(repository); final CmsPatchset patchset = new CmsPatchset(repository, baseRevision); - final boolean overwrite = Boolean.valueOf(config.getParams().get("overwrite")); final CmsItemPropertiesMap properties = config.getItemPropertiesMap(); - final Set locked = new HashSet<>(); + final boolean overwrite = Boolean.parseBoolean(config.getParams().get("overwrite")); + if (overwrite) { + throw new IllegalArgumentException("The overwrite option is currently not supported."); + } + + final String pathext = config.getParams().get("pathext"); + if (pathext == null || pathext.isEmpty()) { + throw new IllegalArgumentException("No pathext parameter was supplied."); + } try { - DownloadResult downloadResult = download(url); - CmsItemLock lock = addToPatchset(patchset, item, downloadResult, overwrite, properties); - if (lock != null) { - locked.add(lock); + if (!itemLookup.getItem(item).getKind().isFolder()) { + throw new IllegalArgumentException("Item must be an existing folder: " + item); } + } catch (CmsItemNotFoundException e) { + throw new IllegalArgumentException("Item must be an existing folder: " + item, e); + } - String comment = config.getParams().get("comment"); - if (comment != null && !comment.trim().isEmpty()) { - patchset.setHistoryMessage(comment); - } + final String pathnamebase = config.getParams().get("pathnamebase"); + final CmsItem location = itemLookup.getItem(repository.getItemId(relPath, null)); + final CmsItemProperties locationProps = location.getProperties(); + final boolean isShardParent = isCmsClass(locationProps, CMS_CLASS_SHARDPARENT); + final boolean hasNamePattern = locationProps.containsProperty(PROPNAME_CONFIG_ITEMNAMEPATTERN); - RepoRevision r = commit.run(patchset); - logger.info("Import complete from URL: {}, committed with rev: {}", url, r.getNumber()); + if (isShardParent && hasNamePattern && pathnamebase != null && !pathnamebase.isEmpty()) { + throw new IllegalStateException("The pathnamebase is not allowed when the folder is configured a shardparent with a name pattern."); + } + if (isShardParent && hasNamePattern) { + CmsItemNaming itemNaming = new CmsItemNamingShard1K(repository, itemLookup); + CmsItemNamePattern namePattern = new CmsItemNamePattern(locationProps.getString(PROPNAME_CONFIG_ITEMNAMEPATTERN)); + relPath = itemNaming.getItemPath(relPath, namePattern, pathext); + } else if (pathnamebase != null && !pathnamebase.isEmpty()) { + relPath = relPath.append(String.format("%s.%s", pathnamebase, pathext)); + } else { + throw new IllegalStateException("Either the folder must be configured a shardparent with a name pattern or a pathnamebase parameter must be supplied."); + } + + final Set locked = new HashSet<>(); + try { + InputStream stream; + if (url != null && !url.trim().isEmpty()) { + // The file content is to be downloaded from the provided URL + stream = download(url); + } else if (content != null && !content.isEmpty()) { + // The file contents are already provided + stream = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); + } else { + throw new IllegalArgumentException("Import requires either a valid URL or content."); + } + CmsItemLock lock = addToPatchset(patchset, relPath, stream, overwrite, properties); + if (lock != null) locked.add(lock); + String comment = config.getParams().get("comment"); + if (comment != null && !comment.trim().isEmpty()) patchset.setHistoryMessage(comment); + RepoRevision r = commit.run(patchset); + if (url != null && !url.trim().isEmpty()) { + logger.info("Import URL complete: {} -> {}, committed with rev: {}", url, relPath, r.getNumber()); + } else { + logger.info("Importing content complete: {}, committed with rev: {}", relPath, r.getNumber()); + } + response.add(new CmsItemIdArg(repository, relPath).withPegRev(r.getNumber())); } catch (IOException | URISyntaxException | InterruptedException e) { logger.error("Failed to download content from URL: {}", url, e); unlockItemsFailure(locked); @@ -239,9 +283,11 @@ public void importItem(CmsItemId item, TransformImportOptions config) { unlockItemsFailure(locked); throw e; } + + return response; } - private DownloadResult download(String url) throws IOException, URISyntaxException, InterruptedException { + private InputStream download(String url) throws IOException, URISyntaxException, InterruptedException { HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofMillis(HTTP_URL_CONNECTION_CONNECT_TIMEOUT)) .followRedirects(HttpClient.Redirect.NORMAL) @@ -258,67 +304,12 @@ private DownloadResult download(String url) throws IOException, URISyntaxExcepti HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofInputStream()); if (response.statusCode() == 200) { - String contentType = response.headers().firstValue("Content-Type").orElse(null); - String extension = getExtensionFromContentType(contentType); - return new DownloadResult(response.body(), contentType, extension); + return response.body(); } else { throw new IOException("HTTP request failed with response code: " + response.statusCode() + " for URL: " + url); } } - private String getExtensionFromContentType(String contentType) { - if (contentType == null) return null; - String mimeType = contentType.toLowerCase().split(";")[0].trim(); - switch (mimeType) { - // Images - case "image/jpeg": return "jpg"; - case "image/png": return "png"; - case "image/gif": return "gif"; - case "image/svg+xml": return "svg"; - case "image/webp": return "webp"; - case "image/bmp": return "bmp"; - case "image/tiff": return "tiff"; - case "image/x-icon": return "ico"; - - // Text/Markup - case "text/xml": - case "application/xml": return "xml"; - case "application/json": return "json"; - case "text/plain": return "txt"; - case "text/html": return "html"; - case "text/css": return "css"; - case "text/csv": return "csv"; - - // Documents - case "application/pdf": return "pdf"; - case "application/msword": return "doc"; - case "application/vnd.openxmlformats-officedocument.wordprocessingml.document": return "docx"; - case "application/vnd.ms-excel": return "xls"; - case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": return "xlsx"; - case "application/vnd.ms-powerpoint": return "ppt"; - case "application/vnd.openxmlformats-officedocument.presentationml.presentation": return "pptx"; - case "application/rtf": return "rtf"; - - // Archives - case "application/zip": return "zip"; - case "application/x-rar-compressed": return "rar"; - case "application/x-tar": return "tar"; - case "application/gzip": return "gz"; - - // Code/Scripts - case "application/javascript": - case "text/javascript": return "js"; - - // Audio/Video - case "audio/mpeg": return "mp3"; - case "audio/wav": return "wav"; - case "video/mp4": return "mp4"; - case "video/mpeg": return "mpeg"; - case "video/quicktime": return "mov"; - - default: return null; - } - } private boolean isTransformable(CmsItemId itemId) { @@ -345,7 +336,7 @@ private Set transformItem(CmsItemId baseItemId, TransformConfig con try { TransformStreamProvider baseStreamProvider = transformerService.getTransformStreamProvider(baseItemId, transformOptions); - locked.add(addToPatchset(patchset, outputPath.append(baseItemId.getRelPath().getName()), baseStreamProvider, overwrite, props)); + locked.add(addToPatchset(patchset, outputPath.append(baseItemId.getRelPath().getName()), baseStreamProvider.get(), overwrite, props)); Set resultDocsHrefs = outputURIResolver.getResultDocumentHrefs(); for (String href: resultDocsHrefs) { @@ -358,7 +349,7 @@ private Set transformItem(CmsItemId baseItemId, TransformConfig con String decodedHref = decodeHref(href); // Items will be commited with decoded hrefs. CmsItemPath path = outputPath.append(Arrays.asList(decodedHref.split("/"))); - locked.add(addToPatchset(patchset, path, streamProvider, overwrite, props)); + locked.add(addToPatchset(patchset, path, streamProvider.get(), overwrite, props)); } } catch (RuntimeException e) { // Unlock locks taken in this invocation of transformItem. @@ -404,88 +395,17 @@ private String getCompleteMessageString(String comment, List messages) { } return sb.toString(); } - - private CmsItemLock addToPatchset(CmsPatchset patchset, CmsItemId itemId, DownloadResult downloadResult, boolean overwrite, CmsItemPropertiesMap properties) { - boolean pathExists; - boolean isFolder = false; - CmsItemLock lock = null; - CmsItemPath relPath = itemId.getRelPath(); - CmsRepository repository = itemId.getRepository(); - InputStream inputStream = downloadResult.getInputStream(); - CmsItemNaming itemNaming = new CmsItemNamingShard1K(repository, itemLookup); - // Determine whether the given path exists and if it is a folder - try { - CmsItem item = itemLookup.getItem(itemId); - isFolder = item.getKind().isFolder(); - pathExists = true; - } catch (CmsItemNotFoundException e) { - pathExists = false; - } - try { - final InputStream contentStream = getInputStreamNotEmpty(inputStream); - if (!pathExists) { - addFolderExists(patchset, relPath.getParent()); - logger.debug("No file at path: '{}' will add new file.", relPath); - FileAdd fileAdd = new FileAdd(relPath, contentStream); - fileAdd.setPropertyChange(properties); - patchset.add(fileAdd); - } else if (isFolder) { - // The path exists and is a folder - addFolderExists(patchset, relPath); - // Auto-name the file - CmsItem location = itemLookup.getItem(repository.getItemId(relPath, null)); - CmsItemProperties locationProps = location.getProperties(); - if (!isCmsClass(locationProps, CMS_CLASS_SHARDPARENT)) { - String msg = MessageFormatter.format("Location is not a shardparent: {}", relPath).getMessage(); - logger.error(msg); - throw new IllegalArgumentException(msg); - } - // The cmsconfig name pattern overrides the bursting rule. - if (!locationProps.containsProperty(PROPNAME_CONFIG_ITEMNAMEPATTERN)) { - String msg = MessageFormatter.format("Location does not define a name pattern: {}", relPath).getMessage(); - logger.error(msg); - throw new IllegalArgumentException(msg); - } - CmsItemNamePattern namePattern = new CmsItemNamePattern(locationProps.getString(PROPNAME_CONFIG_ITEMNAMEPATTERN)); - CmsItemPath itemPath = itemNaming.getItemPath(relPath, namePattern, "jpg"); - logger.debug("No file at path: '{}' will add new file.", itemPath); - addFolderExists(patchset, itemPath.getParent()); - FileAdd fileAdd = new FileAdd(itemPath, contentStream); - fileAdd.setPropertyChange(properties); - patchset.add(fileAdd); - } else if (overwrite) { - // The file exists and is, and we are allowed to overwrite - logger.debug("Overwrite is allowed, existing file at path '{}' will be modified.", relPath.getPath()); - CmsItemLockCollection locks = commit.lock(TRANSFORM_LOCK_COMMENT, patchset.getBaseRevision(), itemId.getRelPath()); - if (locks != null && locks.getSingle() == null) { - throw new IllegalStateException("Unable to retrieve the lock token after locking " + itemId); - } - lock = locks.getSingle(); - patchset.addLock(lock); - FileModificationLocked fileMod = new FileModificationLocked(relPath, contentStream); - fileMod.setPropertyChange(properties); - patchset.add(fileMod); - } else { - // The file exists, and we are not allowed to overwrite it - throw new IllegalStateException("Item already exists, config prohibiting overwrite of existing items."); - } - } catch (IOException e) { - throw new RuntimeException("Failed to read stream from import.", e); - } catch (EmptyStreamException e) { - logger.warn("Import of item at path: '{}' resulted in empty document, will be discarded.", relPath); - } - return lock; - } - private CmsItemLock addToPatchset(CmsPatchset patchset, CmsItemPath relPath, TransformStreamProvider streamProvider, boolean overwrite, CmsItemPropertiesMap properties) { + + private CmsItemLock addToPatchset(CmsPatchset patchset, CmsItemPath relPath, InputStream stream, boolean overwrite, CmsItemPropertiesMap properties) { CmsItemLock lock = null; try { - final InputStream transformStream = getInputStreamNotEmpty(streamProvider.get()); + final InputStream inputStream = getInputStreamNotEmpty(stream); boolean pathExists = pathExists(patchset.getRepository(), relPath); if (!pathExists) { addFolderExists(patchset, relPath.getParent()); logger.debug("No file at path: '{}' will add new file.", relPath); - FileAdd fileAdd = new FileAdd(relPath, transformStream); + FileAdd fileAdd = new FileAdd(relPath, inputStream); fileAdd.setPropertyChange(properties); patchset.add(fileAdd); } else if (overwrite){ @@ -497,7 +417,7 @@ private CmsItemLock addToPatchset(CmsPatchset patchset, CmsItemPath relPath, Tra } lock = locks.getSingle(); patchset.addLock(lock); - FileModificationLocked fileMod = new FileModificationLocked(relPath, transformStream); + FileModificationLocked fileMod = new FileModificationLocked(relPath, inputStream); fileMod.setPropertyChange(properties); patchset.add(fileMod); } else { @@ -647,20 +567,4 @@ public EmptyStreamException(String message) { } } - - private class DownloadResult { - private final InputStream inputStream; - private final String contentType; - private final String extension; - - public DownloadResult(InputStream inputStream, String contentType, String extension) { - this.inputStream = inputStream; - this.contentType = contentType; - this.extension = extension; - } - - public InputStream getInputStream() { return inputStream; } - public String getContentType() { return contentType; } - public String getExtension() { return extension; } - } } From 54de8a5d3142fc18a719cf5a6511eb2ec18b3054 Mon Sep 17 00:00:00 2001 From: Omid Manikhi Date: Tue, 21 Oct 2025 10:47:25 +0200 Subject: [PATCH 13/14] Make sure the item is fully qualified with a hostname in TransformResource. --- .../cms/transform/rest/TransformResource.java | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/main/java/se/simonsoft/cms/transform/rest/TransformResource.java b/src/main/java/se/simonsoft/cms/transform/rest/TransformResource.java index 4b5f749..d19ad17 100644 --- a/src/main/java/se/simonsoft/cms/transform/rest/TransformResource.java +++ b/src/main/java/se/simonsoft/cms/transform/rest/TransformResource.java @@ -16,6 +16,7 @@ package se.simonsoft.cms.transform.rest; import javax.inject.Inject; +import javax.inject.Named; import javax.ws.rs.Consumes; import javax.ws.rs.POST; import javax.ws.rs.Path; @@ -46,11 +47,16 @@ public class TransformResource { private final Logger logger = LoggerFactory.getLogger(TransformResource.class); private final Map transformServiceMap; private final ObjectWriter objectWriter; + private String hostname; private static final int MAX_CONTENT_SIZE_MB = 5; @Inject - public TransformResource(Map transformServiceMap, ObjectWriter objectWriter) { + public TransformResource( + @Named("config:se.simonsoft.cms.hostname") String hostname, + Map transformServiceMap, + ObjectWriter objectWriter) { + this.hostname = hostname; this.transformServiceMap = transformServiceMap; this.objectWriter = objectWriter; } @@ -59,12 +65,14 @@ public TransformResource(Map transformServiceMa @Path("api/import") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - public Response importItem(@QueryParam("item") CmsItemIdArg itemId, String body) throws JsonProcessingException { + public Response importItem(@QueryParam("item") CmsItemIdArg item, String body) throws JsonProcessingException { - if (itemId == null) { + if (item == null) { throw new IllegalArgumentException("Field 'item': required"); } + item.setHostnameOrValidate(hostname); + TransformImportOptions importOptions; try { @@ -92,14 +100,14 @@ public Response importItem(@QueryParam("item") CmsItemIdArg itemId, String body) try { Map> response = new HashMap<>(); - Set items = transformServiceMap.get(itemId.getRepository()).importItem(itemId, importOptions); + Set items = transformServiceMap.get(item.getRepository()).importItem(item, importOptions); response.put("items", items); return Response.ok().entity(objectWriter.writeValueAsString(response)).build(); } catch (IllegalArgumentException e) { - logger.warn("Invalid input parameters for itemId: {}, importOptions: {}", itemId, body, e); + logger.warn("Invalid input parameters for item: {}, importOptions: {}", item, body, e); throw e; } catch (Exception e) { - logger.error("Import failed for itemId: {}, error: {}", itemId, e.getMessage()); + logger.error("Import failed for item: {}, error: {}", item, e.getMessage()); throw e; } } From e3b22d35b9858666d98d21e95be674e2d1b01467 Mon Sep 17 00:00:00 2001 From: Omid Manikhi Date: Tue, 21 Oct 2025 14:09:42 +0200 Subject: [PATCH 14/14] A name pattern is required when the folder is a shard parent. --- .../cms/transform/service/TransformServiceXsl.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/java/se/simonsoft/cms/transform/service/TransformServiceXsl.java b/src/main/java/se/simonsoft/cms/transform/service/TransformServiceXsl.java index b1c74a6..774e556 100644 --- a/src/main/java/se/simonsoft/cms/transform/service/TransformServiceXsl.java +++ b/src/main/java/se/simonsoft/cms/transform/service/TransformServiceXsl.java @@ -39,6 +39,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.slf4j.helpers.MessageFormatter; import se.simonsoft.cms.item.CmsItem; import se.simonsoft.cms.item.CmsItemId; import se.simonsoft.cms.item.CmsItemKind; @@ -205,6 +206,7 @@ public Set importItem(CmsItemId item, TransformImportOptions config) } CmsItemPath relPath = item.getRelPath(); + CmsItemPath parentFolder = relPath.getParent(); final String url = config.getUrl(); final String content = config.getContent(); @@ -234,18 +236,18 @@ public Set importItem(CmsItemId item, TransformImportOptions config) final String pathnamebase = config.getParams().get("pathnamebase"); final CmsItem location = itemLookup.getItem(repository.getItemId(relPath, null)); final CmsItemProperties locationProps = location.getProperties(); + final boolean isShardParent = isCmsClass(locationProps, CMS_CLASS_SHARDPARENT); final boolean hasNamePattern = locationProps.containsProperty(PROPNAME_CONFIG_ITEMNAMEPATTERN); + final boolean hasPathnamebase = pathnamebase != null && !pathnamebase.isEmpty(); - if (isShardParent && hasNamePattern && pathnamebase != null && !pathnamebase.isEmpty()) { - throw new IllegalStateException("The pathnamebase is not allowed when the folder is configured a shardparent with a name pattern."); - } - - if (isShardParent && hasNamePattern) { + if (isShardParent) { + if (!hasNamePattern) throw new IllegalArgumentException(MessageFormatter.format("Location does not define a name pattern: {}", parentFolder).getMessage()); + if (hasPathnamebase) throw new IllegalStateException("The pathnamebase is not allowed when the folder is configured a shardparent with a name pattern."); CmsItemNaming itemNaming = new CmsItemNamingShard1K(repository, itemLookup); CmsItemNamePattern namePattern = new CmsItemNamePattern(locationProps.getString(PROPNAME_CONFIG_ITEMNAMEPATTERN)); relPath = itemNaming.getItemPath(relPath, namePattern, pathext); - } else if (pathnamebase != null && !pathnamebase.isEmpty()) { + } else if (hasPathnamebase) { relPath = relPath.append(String.format("%s.%s", pathnamebase, pathext)); } else { throw new IllegalStateException("Either the folder must be configured a shardparent with a name pattern or a pathnamebase parameter must be supplied.");