From 755271cc42bf25c108f10c16db73da68332808c9 Mon Sep 17 00:00:00 2001 From: "Brian M. Long" Date: Fri, 3 Mar 2023 16:02:39 -0500 Subject: [PATCH] initial checkin --- .gitignore | 13 + pom.xml | 88 +++++ rad.ps1 | 74 +++++ rad.sh | 71 ++++ .../alfresco/bulk/AbstractNodesWebScript.java | 136 ++++++++ .../alfresco/bulk/CreateNodesWebScript.java | 305 ++++++++++++++++++ .../alfresco/bulk/UpdateNodesWebScript.java | 305 ++++++++++++++++++ .../alfresco/bulk/model/AssociationBody.java | 73 +++++ .../bulk/model/ChildAssociationBody.java | 73 +++++ .../alfresco/bulk/model/ContentDataBody.java | 73 +++++ .../alfresco/bulk/model/NodeBodyCreate.java | 230 +++++++++++++ .../bulk/model/NodeBodyCreateAssociation.java | 51 +++ .../bulk/model/NodeBodyCreateExt.java | 49 +++ .../alfresco/bulk/model/NodeBodyUpdate.java | 154 +++++++++ .../bulk/model/NodeBodyUpdateExt.java | 99 ++++++ .../bulk/model/PermissionElement.java | 134 ++++++++ .../bulk/model/PermissionsBodyUpdate.java | 79 +++++ .../alfresco/bulk/createNodes.post.desc.xml | 18 ++ .../alfresco/bulk/updateNodes.put.desc.xml | 18 ++ .../alfresco-global.properties | 8 + .../log4j.properties | 1 + .../module-context.xml | 13 + .../module.properties | 4 + .../alfresco/extension/debug-log4j.properties | 8 + .../disable-webscript-caching-context.xml | 63 ++++ 25 files changed, 2140 insertions(+) create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 rad.ps1 create mode 100644 rad.sh create mode 100644 src/main/java/com/inteligr8/alfresco/bulk/AbstractNodesWebScript.java create mode 100644 src/main/java/com/inteligr8/alfresco/bulk/CreateNodesWebScript.java create mode 100644 src/main/java/com/inteligr8/alfresco/bulk/UpdateNodesWebScript.java create mode 100644 src/main/java/com/inteligr8/alfresco/bulk/model/AssociationBody.java create mode 100644 src/main/java/com/inteligr8/alfresco/bulk/model/ChildAssociationBody.java create mode 100644 src/main/java/com/inteligr8/alfresco/bulk/model/ContentDataBody.java create mode 100644 src/main/java/com/inteligr8/alfresco/bulk/model/NodeBodyCreate.java create mode 100644 src/main/java/com/inteligr8/alfresco/bulk/model/NodeBodyCreateAssociation.java create mode 100644 src/main/java/com/inteligr8/alfresco/bulk/model/NodeBodyCreateExt.java create mode 100644 src/main/java/com/inteligr8/alfresco/bulk/model/NodeBodyUpdate.java create mode 100644 src/main/java/com/inteligr8/alfresco/bulk/model/NodeBodyUpdateExt.java create mode 100644 src/main/java/com/inteligr8/alfresco/bulk/model/PermissionElement.java create mode 100644 src/main/java/com/inteligr8/alfresco/bulk/model/PermissionsBodyUpdate.java create mode 100644 src/main/resources/alfresco/extension/templates/webscripts/com/inteligr8/alfresco/bulk/createNodes.post.desc.xml create mode 100644 src/main/resources/alfresco/extension/templates/webscripts/com/inteligr8/alfresco/bulk/updateNodes.put.desc.xml create mode 100644 src/main/resources/alfresco/module/com.inteligr8.alfresco.bulk-acs-module/alfresco-global.properties create mode 100644 src/main/resources/alfresco/module/com.inteligr8.alfresco.bulk-acs-module/log4j.properties create mode 100644 src/main/resources/alfresco/module/com.inteligr8.alfresco.bulk-acs-module/module-context.xml create mode 100644 src/main/resources/alfresco/module/com.inteligr8.alfresco.bulk-acs-module/module.properties create mode 100644 src/test/resources/alfresco/extension/debug-log4j.properties create mode 100644 src/test/resources/alfresco/extension/disable-webscript-caching-context.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4e28014 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# Maven +target +pom.xml.versionsBackup + +# Eclipse +.settings +.project +.classpath + +# Visual Studio Code +.factorypath +.vscode + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..fd769c6 --- /dev/null +++ b/pom.xml @@ -0,0 +1,88 @@ + + 4.0.0 + + com.inteligr8.alfresco + bulk-platform-module + 1.0-SNAPSHOT + jar + + bulk ACS Platform Module + + + UTF-8 + 8 + 8 + + 4.2.0 + 6.2.0-ga + + + + + + org.alfresco + acs-community-packaging + ${alfresco.platform.version} + pom + import + + + + + + + + + org.alfresco + alfresco-repository + provided + + + io.swagger + swagger-jaxrs + 1.6.2 + provided + + + + + + + io.repaint.maven + tiles-maven-plugin + 2.26 + true + + + + com.inteligr8.ootbee:beedk-acs-platform-self-rad-tile:[1.0.0,2.0.0) + + com.inteligr8.ootbee:beedk-acs-platform-module-tile:[1.0.0,2.0.0) + + + + + + + + + + inteligr8-releases + http://repos.inteligr8.com/nexus/repository/inteligr8-public + + + alfresco-public + https://artifacts.alfresco.com/nexus/content/groups/public + + + + + + inteligr8-releases + http://repos.inteligr8.com/nexus/repository/inteligr8-public + + + \ No newline at end of file diff --git a/rad.ps1 b/rad.ps1 new file mode 100644 index 0000000..61bcb2f --- /dev/null +++ b/rad.ps1 @@ -0,0 +1,74 @@ + +function discoverArtifactId { + $script:ARTIFACT_ID=(mvn -q -Dexpression=project"."artifactId -DforceStdout help:evaluate) +} + +function rebuild { + echo "Rebuilding project ..." + mvn process-classes +} + +function start_ { + echo "Rebuilding project and starting Docker containers to support rapid application development ..." + mvn -Drad process-classes +} + +function start_log { + echo "Rebuilding project and starting Docker containers to support rapid application development ..." + mvn -Drad "-Ddocker.showLogs" process-classes +} + +function stop_ { + discoverArtifactId + echo "Stopping Docker containers that supported rapid application development ..." + docker container ls --filter name=${ARTIFACT_ID}-* + echo "Stopping containers ..." + docker container stop (docker container ls -q --filter name=${ARTIFACT_ID}-*) + echo "Removing containers ..." + docker container rm (docker container ls -aq --filter name=${ARTIFACT_ID}-*) +} + +function tail_logs { + param ( + $container + ) + + discoverArtifactId + docker container logs -f (docker container ls -q --filter name=${ARTIFACT_ID}-${container}) +} + +function list { + discoverArtifactId + docker container ls --filter name=${ARTIFACT_ID}-* +} + +switch ($args[0]) { + "start" { + start_ + } + "start_log" { + start_log + } + "stop" { + stop_ + } + "restart" { + stop_ + start_ + } + "rebuild" { + rebuild + } + "tail" { + tail_logs $args[1] + } + "containers" { + list + } + default { + echo "Usage: .\rad.ps1 [ start | start_log | stop | restart | rebuild | tail {container} | containers ]" + } +} + +echo "Completed!" + diff --git a/rad.sh b/rad.sh new file mode 100644 index 0000000..7cb0a80 --- /dev/null +++ b/rad.sh @@ -0,0 +1,71 @@ +#!/bin/sh + +discoverArtifactId() { + ARTIFACT_ID=`mvn -q -Dexpression=project.artifactId -DforceStdout help:evaluate` +} + +rebuild() { + echo "Rebuilding project ..." + mvn process-classes +} + +start() { + echo "Rebuilding project and starting Docker containers to support rapid application development ..." + mvn -Drad process-classes +} + +start_log() { + echo "Rebuilding project and starting Docker containers to support rapid application development ..." + mvn -Drad -Ddocker.showLogs process-classes +} + +stop() { + discoverArtifactId + echo "Stopping Docker containers that supported rapid application development ..." + docker container ls --filter name=${ARTIFACT_ID}-* + echo "Stopping containers ..." + docker container stop `docker container ls -q --filter name=${ARTIFACT_ID}-*` + echo "Removing containers ..." + docker container rm `docker container ls -aq --filter name=${ARTIFACT_ID}-*` +} + +tail_logs() { + discoverArtifactId + docker container logs -f `docker container ls -q --filter name=${ARTIFACT_ID}-$1` +} + +list() { + discoverArtifactId + docker container ls --filter name=${ARTIFACT_ID}-* +} + +case "$1" in + start) + start + ;; + start_log) + start_log + ;; + stop) + stop + ;; + restart) + stop + start + ;; + rebuild) + rebuild + ;; + tail) + tail_logs $2 + ;; + containers) + list + ;; + *) + echo "Usage: ./rad.sh [ start | start_log | stop | restart | rebuild | tail {container} | containers ]" + exit 1 +esac + +echo "Completed!" + diff --git a/src/main/java/com/inteligr8/alfresco/bulk/AbstractNodesWebScript.java b/src/main/java/com/inteligr8/alfresco/bulk/AbstractNodesWebScript.java new file mode 100644 index 0000000..c5f2d10 --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/bulk/AbstractNodesWebScript.java @@ -0,0 +1,136 @@ +/* + * 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 Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package com.inteligr8.alfresco.bulk; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.policy.BehaviourFilter; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.MimetypeService; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.apache.tika.Tika; +import org.apache.tika.config.TikaConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.extensions.webscripts.AbstractWebScript; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.inteligr8.alfresco.bulk.model.ContentDataBody; + +public abstract class AbstractNodesWebScript extends AbstractWebScript { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Value("${inteligr8.bulk.enableTika:false}") + private boolean enableTika; + + @Value("${inteligr8.bulk.allowOctetStream:false}") + protected boolean allowOctetStream; + + @Autowired + protected ContentService contentService; + + @Autowired + protected DictionaryService dictionaryService; + + @Autowired + protected MimetypeService mimeTypeService; + + @Autowired + protected NamespaceService namespaceService; + + @Autowired + protected NodeService nodeService; + + @Autowired + protected BehaviourFilter behaviorFilter; + + @Autowired + protected TransactionService txService; + + protected final ObjectMapper om = new ObjectMapper(); + + protected final Collection jsonMediaTypes = + MediaType.parseMediaTypes(Arrays.asList(MediaType.APPLICATION_JSON_VALUE, "applicatin/*+json")); + + protected List wrapContents(ContentDataBody contentData, List contents) { + List allcontents = new ArrayList<>(contents == null ? 1 : (contents.size() + 1)); + if (contentData != null) + allcontents.add(contentData); + if (contents != null) + allcontents.addAll(contents); + return allcontents; + } + + protected void validateRequest(List nodes) { + if (nodes == null || nodes.isEmpty()) + throw new WebScriptException(HttpStatus.BAD_REQUEST.value(), "At least one node is required"); + } + + protected String determineMimeType(QName contentPropName, ContentReader creader, String name) { + if (this.enableTika) { + this.logger.trace("Using Tika to detect the MIME type: {} => {}", creader.getContentUrl(), name); + + try { + InputStream istream = creader.getContentInputStream(); + try { + Tika tika = new Tika(TikaConfig.getDefaultConfig()); + String mimeType = tika.detect(istream, name); + this.logger.trace("Tika detected MIME type: {} => {}", creader.getContentUrl(), mimeType); + + if (!MediaType.APPLICATION_OCTET_STREAM_VALUE.equals(mimeType)) + return mimeType; + } finally { + istream.close(); + } + } catch (IOException ie) { + this.logger.warn("Tika MIME detection failed: {}: {}", creader.getContentUrl(), ie.getMessage()); + } + } + + if (!contentPropName.equals(ContentModel.PROP_CONTENT)) { + this.logger.debug("Unable to auto-detect a content property other than the default 'cm:content': {}", contentPropName); + return null; + } + + int pos = name.lastIndexOf('.'); + if (pos < 0) { + this.logger.debug("Unable to auto-detect a MIME type without a filename extension: {}", name); + return null; + } + + String extension = name.substring(pos+1); + String mimeType = this.mimeTypeService.getMimetype(extension); + this.logger.debug("Detected file extension and MIME type: {} => {}", extension, mimeType); + return mimeType; + } + +} diff --git a/src/main/java/com/inteligr8/alfresco/bulk/CreateNodesWebScript.java b/src/main/java/com/inteligr8/alfresco/bulk/CreateNodesWebScript.java new file mode 100644 index 0000000..ccdce21 --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/bulk/CreateNodesWebScript.java @@ -0,0 +1,305 @@ +/* + * 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 Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package com.inteligr8.alfresco.bulk; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.extensions.surf.util.Content; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.inteligr8.alfresco.bulk.model.AssociationBody; +import com.inteligr8.alfresco.bulk.model.ChildAssociationBody; +import com.inteligr8.alfresco.bulk.model.ContentDataBody; +import com.inteligr8.alfresco.bulk.model.NodeBodyCreateExt; + +@Component(value = "webscript.com.inteligr8.alfresco.bulk.createNodes.post") +public class CreateNodesWebScript extends AbstractNodesWebScript { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Override + public void execute(WebScriptRequest req, WebScriptResponse res) throws IOException { + this.logger.trace("execute()"); + + String folderNodeId = req.getServiceMatch().getTemplateVars().get("folderNodeId"); + + MediaType contentType = this.validateAndGetContentType(req.getContentType()); + boolean multipart = MediaType.MULTIPART_FORM_DATA.equals(contentType); + + List createdNodeIds; + if (multipart) { + throw new WebScriptException(HttpStatus.NOT_IMPLEMENTED.value(), "This service does not yet support multipart content"); + } else { + List nodes = this.parseRequest(req.getContent()); + this.validateRequest(nodes); + + createdNodeIds = this.createNodes(folderNodeId, nodes); + } + + res.setStatus(HttpStatus.OK.value()); + res.setContentType(MediaType.APPLICATION_JSON_VALUE); + res.setContentEncoding("utf-8"); + this.om.writeValue(res.getWriter(), createdNodeIds); + } + + protected List createNodes(String parentNodeId, List nodes) { + RetryingTransactionCallback rtcallback = new RetryingTransactionCallback() { + @Override + public Object execute() { + NodeRef parentNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, parentNodeId); + if (!nodeService.exists(parentNodeRef)) + throw new WebScriptException(HttpStatus.NOT_FOUND.value(), "The folder node could not be found: {}", parentNodeId); + if (!dictionaryService.isSubClass(nodeService.getType(parentNodeRef), ContentModel.TYPE_FOLDER)) + throw new WebScriptException(HttpStatus.BAD_REQUEST.value(), "The parent node must be a folder: {}", parentNodeId); + + // allow cm:created/cm:modified to override + behaviorFilter.disableBehaviour(ContentModel.ASPECT_AUDITABLE); + + List createdNodeIds = new LinkedList<>(); + + try { + for (NodeBodyCreateExt node : nodes) { + String nodeId = createNode(parentNodeRef, node); + if (nodeId != null) + createdNodeIds.add(nodeId); + } + } catch (WebScriptException wse) { + return wse; + } + + behaviorFilter.enableBehaviour(ContentModel.ASPECT_AUDITABLE); + + return createdNodeIds; + } + }; + + Object response = this.txService.getRetryingTransactionHelper().doInTransaction(rtcallback, false, false); + if (response instanceof WebScriptException) + throw (WebScriptException) response; + return (List) response; + } + + protected String createNode(NodeRef parentNodeRef, NodeBodyCreateExt incomingNode) { + this.logger.debug("Attempting to create node: {} in {}", incomingNode.getName(), parentNodeRef); + + Map incomingProps = (Map) incomingNode.getProperties(); + + String name = incomingNode.getName(); + if (name == null) { + name = incomingProps == null ? null : (String) incomingProps.get(ContentModel.PROP_NAME.toPrefixString(this.namespaceService)); + if (name == null) + throw new WebScriptException(HttpStatus.BAD_REQUEST.value(), "A name or 'cm:name' property is required"); + } + this.logger.trace("Using name: {}", name); + + boolean hasContent = incomingNode.getContentData() != null || (incomingNode.getContents() != null && !incomingNode.getContents().isEmpty()); + QName nodeType = incomingNode.getNodeType() == null + ? (hasContent ? ContentModel.TYPE_CONTENT : ContentModel.TYPE_FOLDER) + : QName.createQName(incomingNode.getNodeType(), this.namespaceService); + this.logger.trace("Using node type: {}", nodeType); + + Map props = null; + if (incomingProps != null) { + props = new HashMap<>(incomingProps.size()+1); + for (Entry prop : incomingProps.entrySet()) { + QName propName = QName.createQName(prop.getKey(), this.namespaceService); + props.put(propName, prop.getValue()); + } + + if (incomingNode.getName() != null) + props.put(ContentModel.PROP_NAME, name); + + // audit is disable, so initialize if not set + if (!props.containsKey(ContentModel.PROP_CREATED)) + props.put(ContentModel.PROP_CREATED, new Date()); + if (!props.containsKey(ContentModel.PROP_MODIFIED)) + props.put(ContentModel.PROP_MODIFIED, new Date()); + if (!props.containsKey(ContentModel.PROP_CREATOR)) + props.put(ContentModel.PROP_CREATOR, AuthenticationUtil.getFullyAuthenticatedUser()); + if (!props.containsKey(ContentModel.PROP_MODIFIER)) + props.put(ContentModel.PROP_MODIFIER, AuthenticationUtil.getFullyAuthenticatedUser()); + } + this.logger.trace("Using properties: {}", props.keySet()); + + QName assocType = incomingNode.getAssociation() == null + ? ContentModel.ASSOC_CONTAINS + : QName.createQName(incomingNode.getAssociation().getAssocType(), this.namespaceService); + QName qname = QName.createQNameWithValidLocalName(NamespaceService.CONTENT_MODEL_1_0_URI, name); + this.logger.trace("Using parent association type and name: {} => {}", props.keySet(), qname); + + ChildAssociationRef parentRef = this.nodeService.createNode(parentNodeRef, assocType, qname, nodeType, props); + NodeRef nodeRef = parentRef.getChildRef(); + this.logger.debug("Created node: {}", nodeRef); + + this.addNodeAspects(nodeRef, incomingNode.getAspectNames()); + this.addNodeAssociations(nodeRef, incomingNode.getTargets()); + this.addNodeSecondaryAssociations(nodeRef, incomingNode.getSecondaryChildren()); + this.addNodeContents(nodeRef, this.wrapContents(incomingNode.getContentData(), incomingNode.getContents()), name); + + return nodeRef.getId(); + } + + private int addNodeAspects(NodeRef nodeRef, List incomingAspects) { + int changes = 0; + if (incomingAspects == null) + return changes; + + this.logger.debug("Adding aspects: {}: {}", nodeRef, incomingAspects); + + for (String aspectName : incomingAspects) { + QName aspectQName = QName.createQName(aspectName, this.namespaceService); + this.nodeService.addAspect(nodeRef, aspectQName, null); + changes++; + } + + return changes; + } + + private int addNodeAssociations(NodeRef nodeRef, List incomingAssocs) { + int changes = 0; + if (incomingAssocs == null) + return changes; + + this.logger.debug("Adding associations: {}: {}", nodeRef, incomingAssocs); + + for (AssociationBody assoc : incomingAssocs) { + NodeRef targetNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, assoc.getTargetId()); + QName assocType = QName.createQName(assoc.getAssocType(), this.namespaceService); + this.nodeService.createAssociation(nodeRef, targetNodeRef, assocType); + changes++; + } + + return changes; + } + + private int addNodeSecondaryAssociations(NodeRef nodeRef, List incomingAssocs) { + int changes = 0; + if (incomingAssocs == null) + return changes; + + this.logger.debug("Adding child associations: {}: {}", nodeRef, incomingAssocs); + + for (ChildAssociationBody assoc : incomingAssocs) { + NodeRef childNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, assoc.getChildId()); + ChildAssociationRef childParentRef = this.nodeService.getPrimaryParent(childNodeRef); + QName assocType = QName.createQName(assoc.getAssocType(), this.namespaceService); + this.nodeService.addChild(nodeRef, childNodeRef, assocType, childParentRef.getQName()); + changes++; + } + + return changes; + } + + private int addNodeContents(NodeRef nodeRef, List incomingContents, String name) { + int changes = 0; + if (incomingContents.isEmpty()) + return changes; + + Set contentPropsProcessed = new HashSet<>(); + + for (ContentDataBody content : incomingContents) { + QName contentPropName = content.getPropertyName() == null ? + ContentModel.PROP_CONTENT : + QName.createQName(content.getPropertyName(), this.namespaceService); + if (!contentPropsProcessed.add(contentPropName)) + throw new WebScriptException(HttpStatus.BAD_REQUEST.value(), "The same content property was specified more than once on a node: " + nodeRef); + if (content.getContentUrl() == null) + throw new WebScriptException(HttpStatus.BAD_REQUEST.value(), "In-place node creation requires a content URL: " + nodeRef); + + // this will allow us to get the size without opening a stream + // it may also be used to determine the MIME type + ContentReader creader = this.contentService.getRawReader(content.getContentUrl()); + + if (content.getMimeType() == null) { + String mimeType = this.determineMimeType(contentPropName, creader, name); + if (mimeType == null) + throw new WebScriptException(HttpStatus.BAD_REQUEST.value(), "In-place node creation requires a MIME type: " + nodeRef); + if (!this.allowOctetStream && mimeType.equals(MediaType.APPLICATION_OCTET_STREAM_VALUE)) + throw new WebScriptException(HttpStatus.BAD_REQUEST.value(), "MIME type '" + mimeType + "' is not allowed by configuration of this module"); + + content.setMimeType(mimeType); + } + + ContentData contentData = new ContentData(content.getContentUrl(), + content.getMimeType(), creader.getSize(), content.getEncoding(), Locale.getDefault()); + + this.nodeService.setProperty(nodeRef, contentPropName, contentData); + changes++; + } + + return changes; + } + + private MediaType validateAndGetContentType(String contentType) { + contentType = StringUtils.trimToNull(contentType); + this.logger.debug("Received content type: {}", contentType); + + if (contentType == null) + throw new WebScriptException(HttpStatus.BAD_REQUEST.value(), "A content type is required"); + + MediaType incomingMediaType = MediaType.parseMediaType(contentType); + for (MediaType jsonMediaType : this.jsonMediaTypes) { + if (jsonMediaType.includes(incomingMediaType)) { + return MediaType.APPLICATION_JSON; + } + } + + if (MediaType.MULTIPART_FORM_DATA.equals(incomingMediaType)) + return incomingMediaType; + + throw new WebScriptException(HttpStatus.BAD_REQUEST.value(), "A JSON or multipart content type is required"); + } + + private List parseRequest(Content content) throws IOException { + this.logger.debug("Received content of size: {}", content.getSize()); + InputStream istream = content.getInputStream(); + try { + return this.om.readValue(istream, new TypeReference>() {}); + } finally { + istream.close(); + } + } + +} diff --git a/src/main/java/com/inteligr8/alfresco/bulk/UpdateNodesWebScript.java b/src/main/java/com/inteligr8/alfresco/bulk/UpdateNodesWebScript.java new file mode 100644 index 0000000..58c9024 --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/bulk/UpdateNodesWebScript.java @@ -0,0 +1,305 @@ +/* + * 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 Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ +package com.inteligr8.alfresco.bulk; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.extensions.surf.util.Content; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; +import org.springframework.extensions.webscripts.WebScriptResponse; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.inteligr8.alfresco.bulk.model.AssociationBody; +import com.inteligr8.alfresco.bulk.model.ContentDataBody; +import com.inteligr8.alfresco.bulk.model.NodeBodyUpdateExt; +import com.inteligr8.alfresco.bulk.model.PermissionsBodyUpdate; + +@Component(value = "webscript.com.inteligr8.alfresco.bulk.updateNodes.put") +public class UpdateNodesWebScript extends AbstractNodesWebScript { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Override + public void execute(WebScriptRequest req, WebScriptResponse res) throws IOException { + this.logger.trace("execute()"); + + this.validateContentType(req.getContentType()); + + List nodes = this.parseRequest(req.getContent()); + this.validateRequest(nodes); + + List updatedNodeIds = this.updateNodes(nodes); + + res.setStatus(HttpStatus.OK.value()); + res.setContentType(MediaType.APPLICATION_JSON_VALUE); + res.setContentEncoding("utf-8"); + this.om.writeValue(res.getWriter(), updatedNodeIds); + } + + protected List updateNodes(List nodes) { + RetryingTransactionCallback rtcallback = new RetryingTransactionCallback() { + @Override + public Object execute() { + List updatedNodeIds = new LinkedList<>(); + + try { + for (NodeBodyUpdateExt node : nodes) { + if (updateNode(node)) + updatedNodeIds.add(node.getId()); + } + } catch (WebScriptException wse) { + return wse; + } + + return updatedNodeIds; + } + }; + + Object response = this.txService.getRetryingTransactionHelper().doInTransaction(rtcallback, false, false); + if (response instanceof WebScriptException) + throw (WebScriptException) response; + return (List) response; + } + + protected boolean updateNode(NodeBodyUpdateExt node) { + NodeRef nodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, node.getId()); + this.logger.debug("Attempting to update node: {}", nodeRef); + + if (!this.nodeService.exists(nodeRef)) + throw new WebScriptException(HttpStatus.NOT_FOUND.value(), "The node could not be found: {}", node.getId()); + + this.updateNodeAspects(nodeRef, node.getAspectNames()); + this.updateNodeType(nodeRef, node.getNodeType()); + this.updateNodeProperties(nodeRef, node.getName(), node.getProperties(), node.isRemoveUnspecifiedProperties()); + this.updateNodeAssociations(nodeRef, node.getTargets()); + this.updateNodeContents(nodeRef, this.wrapContents(node.getContentData(), node.getContents())); + this.updateNodePermissions(nodeRef, node.getPermissions()); + + return true; + } + + private int updateNodeAspects(NodeRef nodeRef, List incomingAspects) { + int changes = 0; + if (incomingAspects == null) + return changes; + + Set currentAspects = this.nodeService.getAspects(nodeRef); + this.logger.trace("Reconciling aspects: {}: {} => {}", nodeRef, currentAspects, incomingAspects); + + for (String aspectName : incomingAspects) { + QName aspectQName = QName.createQName(aspectName, this.namespaceService); + if (!currentAspects.remove(aspectQName)) { + this.logger.debug("Adding aspect: {}: {}", nodeRef, aspectQName); + this.nodeService.addAspect(nodeRef, aspectQName, null); + changes++; + } + } + + for (QName aspectQName : currentAspects) { + this.logger.debug("Removing aspect: {}: {}", nodeRef, aspectQName); + this.nodeService.removeAspect(nodeRef, aspectQName); + changes++; + } + + return changes; + } + + private boolean updateNodeType(NodeRef nodeRef, String incomingNodeTypeName) { + if (incomingNodeTypeName == null) + return false; + + QName currentNodeType = this.nodeService.getType(nodeRef); + this.logger.trace("Reconciling node type: {}: {} => {}", nodeRef, currentNodeType, incomingNodeTypeName); + + QName incomingNodeType = QName.createQName(incomingNodeTypeName, this.namespaceService); + if (!currentNodeType.equals(incomingNodeType)) { + this.logger.debug("Changing node type: {}: {}", nodeRef, incomingNodeType); + this.nodeService.setType(nodeRef, incomingNodeType); + return true; + } + + return false; + } + + private void updateNodeProperties(NodeRef nodeRef, String incomingName, Map incomingProps, boolean removeUnspecProps) { + if (incomingName == null && incomingProps == null) + return; + + Map props; + if (incomingProps == null) { + props = new HashMap<>(1); + } else { + props = new HashMap<>(incomingProps.size()+1); + for (Entry prop : incomingProps.entrySet()) { + QName propName = QName.createQName(prop.getKey(), this.namespaceService); + props.put(propName, prop.getValue()); + } + } + + if (incomingName != null) + props.put(ContentModel.PROP_NAME, incomingName); + + this.logger.debug("Changing properties: {}: {}", nodeRef, props.keySet()); + this.nodeService.addProperties(nodeRef, props); + + if (removeUnspecProps) { + Map currentProps = this.nodeService.getProperties(nodeRef); + for (QName propName : props.keySet()) + currentProps.remove(propName); + + for (QName propName : currentProps.keySet()) { + this.logger.trace("Removing unspecified property: {}: {}", nodeRef, propName); + this.nodeService.removeProperty(nodeRef, propName); + } + } + } + + private int updateNodeAssociations(NodeRef nodeRef, List incomingAssocs) { + int changes = 0; + if (incomingAssocs == null) + return changes; + + Set currentAssocRefs = new HashSet<>(this.nodeService.getTargetAssocs(nodeRef, RegexQNamePattern.MATCH_ALL)); + this.logger.trace("Reconciling associations: {}: {} => {}", nodeRef, currentAssocRefs, incomingAssocs); + + for (AssociationBody assoc : incomingAssocs) { + NodeRef targetNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, assoc.getTargetId()); + QName assocType = QName.createQName(assoc.getAssocType(), this.namespaceService); + AssociationRef incomingAssocRef = new AssociationRef(nodeRef, assocType, targetNodeRef); + + if (!currentAssocRefs.remove(incomingAssocRef)) { + this.logger.debug("Adding association: {}: {}", nodeRef, incomingAssocRef); + this.nodeService.createAssociation(nodeRef, targetNodeRef, assocType); + changes++; + } + } + + for (AssociationRef assocRef : currentAssocRefs) { + this.logger.debug("Removing association: {}: {}", nodeRef, assocRef); + this.nodeService.removeAssociation(assocRef.getSourceRef(), assocRef.getTargetRef(), assocRef.getTypeQName()); + changes++; + } + + return changes; + } + + private int updateNodeContents(NodeRef nodeRef, List incomingContents) { + int changes = 0; + if (incomingContents.isEmpty()) + return changes; + + Set contentPropsProcessed = new HashSet<>(); + + for (ContentDataBody content : incomingContents) { + QName contentPropName = content.getPropertyName() == null ? + ContentModel.PROP_CONTENT : + QName.createQName(content.getPropertyName(), this.namespaceService); + if (!contentPropsProcessed.add(contentPropName)) + throw new WebScriptException(HttpStatus.BAD_REQUEST.value(), "The same content property was specified more than once on a node: " + nodeRef); + + Object contentObj = this.nodeService.getProperty(nodeRef, contentPropName); + if (contentObj == null) { + throw new WebScriptException(HttpStatus.NOT_IMPLEMENTED.value(), "An update does not yet support new content"); + } else if (!(contentObj instanceof ContentData)) { + throw new WebScriptException(HttpStatus.BAD_REQUEST.value(), "A content property does not hold content data: " + contentPropName); + } + + ContentData contentData = (ContentData) contentObj; + boolean changed = false; + + if (content.getMimeType() != null && !content.getMimeType().equals(contentData.getMimetype())) { + if (!this.allowOctetStream && content.getMimeType().equals(MediaType.APPLICATION_OCTET_STREAM_VALUE)) + throw new WebScriptException(HttpStatus.BAD_REQUEST.value(), "MIME type '" + content.getMimeType() + "' is not allowed by configuration of this module"); + + this.logger.debug("Changing content MIME type: {}: {}", nodeRef, content.getMimeType()); + contentData = ContentData.setMimetype(contentData, content.getMimeType()); + changed = true; + } + + if (content.getEncoding() != null && !content.getEncoding().equals(contentData.getEncoding())) { + this.logger.debug("Changing content encoding: {}: {}", nodeRef, content.getEncoding()); + contentData = ContentData.setEncoding(contentData, content.getEncoding()); + changed = true; + } + + if (changed) { + this.nodeService.setProperty(nodeRef, contentPropName, contentData); + changes++; + } + } + + return changes; + } + + private int updateNodePermissions(NodeRef nodeRef, PermissionsBodyUpdate incomingPermissions) { + int changes = 0; + if (incomingPermissions == null) + return changes; + + throw new WebScriptException(HttpStatus.NOT_IMPLEMENTED.value(), "An update does not yet support permissions"); + } + + private void validateContentType(String contentType) { + contentType = StringUtils.trimToNull(contentType); + this.logger.debug("Received content type: {}", contentType); + + if (contentType == null) + throw new WebScriptException(HttpStatus.BAD_REQUEST.value(), "A content type is required"); + + MediaType incomingMediaType = MediaType.parseMediaType(contentType); + for (MediaType jsonMediaType : this.jsonMediaTypes) { + if (jsonMediaType.includes(incomingMediaType)) { + return; + } + } + + throw new WebScriptException(HttpStatus.BAD_REQUEST.value(), "A JSON content type is required"); + } + + private List parseRequest(Content content) throws IOException { + this.logger.debug("Received content of size: {}", content.getSize()); + InputStream istream = content.getInputStream(); + try { + return this.om.readValue(istream, new TypeReference>() {}); + } finally { + istream.close(); + } + } + +} diff --git a/src/main/java/com/inteligr8/alfresco/bulk/model/AssociationBody.java b/src/main/java/com/inteligr8/alfresco/bulk/model/AssociationBody.java new file mode 100644 index 0000000..2e189f6 --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/bulk/model/AssociationBody.java @@ -0,0 +1,73 @@ +package com.inteligr8.alfresco.bulk.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; + +@com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = true) +public class AssociationBody { + + @ApiModelProperty(required = true, value = "") + private String targetId = null; + + @ApiModelProperty(required = true, value = "") + private String assocType = null; + /** + * Get targetId + * @return targetId + **/ + @JsonProperty("targetId") + public String getTargetId() { + return targetId; + } + + public void setTargetId(String targetId) { + this.targetId = targetId; + } + + public AssociationBody targetId(String targetId) { + this.targetId = targetId; + return this; + } + + /** + * Get assocType + * @return assocType + **/ + @JsonProperty("assocType") + public String getAssocType() { + return assocType; + } + + public void setAssocType(String assocType) { + this.assocType = assocType; + } + + public AssociationBody assocType(String assocType) { + this.assocType = assocType; + return this; + } + + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class AssociationBody {\n"); + + sb.append(" targetId: ").append(toIndentedString(targetId)).append("\n"); + sb.append(" assocType: ").append(toIndentedString(assocType)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private static String toIndentedString(java.lang.Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} + diff --git a/src/main/java/com/inteligr8/alfresco/bulk/model/ChildAssociationBody.java b/src/main/java/com/inteligr8/alfresco/bulk/model/ChildAssociationBody.java new file mode 100644 index 0000000..43ddb4e --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/bulk/model/ChildAssociationBody.java @@ -0,0 +1,73 @@ +package com.inteligr8.alfresco.bulk.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; + +@com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = true) +public class ChildAssociationBody { + + @ApiModelProperty(required = true, value = "") + private String childId = null; + + @ApiModelProperty(required = true, value = "") + private String assocType = null; + /** + * Get childId + * @return childId + **/ + @JsonProperty("childId") + public String getChildId() { + return childId; + } + + public void setChildId(String childId) { + this.childId = childId; + } + + public ChildAssociationBody childId(String childId) { + this.childId = childId; + return this; + } + + /** + * Get assocType + * @return assocType + **/ + @JsonProperty("assocType") + public String getAssocType() { + return assocType; + } + + public void setAssocType(String assocType) { + this.assocType = assocType; + } + + public ChildAssociationBody assocType(String assocType) { + this.assocType = assocType; + return this; + } + + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ChildAssociationBody {\n"); + + sb.append(" childId: ").append(toIndentedString(childId)).append("\n"); + sb.append(" assocType: ").append(toIndentedString(assocType)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private static String toIndentedString(java.lang.Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} + diff --git a/src/main/java/com/inteligr8/alfresco/bulk/model/ContentDataBody.java b/src/main/java/com/inteligr8/alfresco/bulk/model/ContentDataBody.java new file mode 100644 index 0000000..ebef87c --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/bulk/model/ContentDataBody.java @@ -0,0 +1,73 @@ +package com.inteligr8.alfresco.bulk.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class ContentDataBody { + + @JsonProperty(defaultValue = "cm:content") + private String propertyName; + + @JsonProperty + private String mimeType; + + @JsonProperty + private String encoding; + + @JsonProperty + private String contentUrl; + + public String getPropertyName() { + return propertyName; + } + + public void setPropertyName(String propertyName) { + this.propertyName = propertyName; + } + + public ContentDataBody propertyName(String propertyName) { + this.propertyName = propertyName; + return this; + } + + public String getMimeType() { + return mimeType; + } + + public void setMimeType(String mimeType) { + this.mimeType = mimeType; + } + + public ContentDataBody mimeType(String mimeType) { + this.mimeType = mimeType; + return this; + } + + public String getEncoding() { + return encoding; + } + + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + public ContentDataBody encoding(String encoding) { + this.encoding = encoding; + return this; + } + + public String getContentUrl() { + return contentUrl; + } + + public void setContentUrl(String contentUrl) { + this.contentUrl = contentUrl; + } + + public ContentDataBody contentUrl(String contentUrl) { + this.contentUrl = contentUrl; + return this; + } + +} diff --git a/src/main/java/com/inteligr8/alfresco/bulk/model/NodeBodyCreate.java b/src/main/java/com/inteligr8/alfresco/bulk/model/NodeBodyCreate.java new file mode 100644 index 0000000..b56f9cf --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/bulk/model/NodeBodyCreate.java @@ -0,0 +1,230 @@ +package com.inteligr8.alfresco.bulk.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; +import java.util.List; +import java.util.Map; + +@com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = true) +public class NodeBodyCreate { + + @ApiModelProperty(required = true, value = "The name must not contain spaces or the following special characters: * \" < > \\ / ? : and |. The character . must not be used at the end of the name. ") + /** + * The name must not contain spaces or the following special characters: * \" < > \\ / ? : and |. The character . must not be used at the end of the name. + **/ + private String name = null; + + @ApiModelProperty(required = true, value = "") + private String nodeType = null; + + @ApiModelProperty(value = "") + private List aspectNames = null; + + @ApiModelProperty(value = "") + private Map properties = null; + + @ApiModelProperty(value = "") + private String relativePath = null; + + @ApiModelProperty(value = "") + private NodeBodyCreateAssociation association = null; + + @ApiModelProperty(value = "") + private List secondaryChildren = null; + + @ApiModelProperty(value = "") + private List targets = null; + /** + * The name must not contain spaces or the following special characters: * \" < > \\ / ? : and |. The character . must not be used at the end of the name. + * @return name + **/ + @JsonProperty("name") + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public NodeBodyCreate name(String name) { + this.name = name; + return this; + } + + /** + * Get nodeType + * @return nodeType + **/ + @JsonProperty("nodeType") + public String getNodeType() { + return nodeType; + } + + public void setNodeType(String nodeType) { + this.nodeType = nodeType; + } + + public NodeBodyCreate nodeType(String nodeType) { + this.nodeType = nodeType; + return this; + } + + /** + * Get aspectNames + * @return aspectNames + **/ + @JsonProperty("aspectNames") + public List getAspectNames() { + return aspectNames; + } + + public void setAspectNames(List aspectNames) { + this.aspectNames = aspectNames; + } + + public NodeBodyCreate aspectNames(List aspectNames) { + this.aspectNames = aspectNames; + return this; + } + + public NodeBodyCreate addAspectNamesItem(String aspectNamesItem) { + this.aspectNames.add(aspectNamesItem); + return this; + } + + /** + * Get properties + * @return properties + **/ + @JsonProperty("properties") + public Map getProperties() { + return properties; + } + + public void setProperties(Map properties) { + this.properties = properties; + } + + public NodeBodyCreate properties(Map properties) { + this.properties = properties; + return this; + } + + public NodeBodyCreate putPropertiesItem(String key, String propertiesItem) { + this.properties.put(key, propertiesItem); + return this; + } + + /** + * Get relativePath + * @return relativePath + **/ + @JsonProperty("relativePath") + public String getRelativePath() { + return relativePath; + } + + public void setRelativePath(String relativePath) { + this.relativePath = relativePath; + } + + public NodeBodyCreate relativePath(String relativePath) { + this.relativePath = relativePath; + return this; + } + + /** + * Get association + * @return association + **/ + @JsonProperty("association") + public NodeBodyCreateAssociation getAssociation() { + return association; + } + + public void setAssociation(NodeBodyCreateAssociation association) { + this.association = association; + } + + public NodeBodyCreate association(NodeBodyCreateAssociation association) { + this.association = association; + return this; + } + + /** + * Get secondaryChildren + * @return secondaryChildren + **/ + @JsonProperty("secondaryChildren") + public List getSecondaryChildren() { + return secondaryChildren; + } + + public void setSecondaryChildren(List secondaryChildren) { + this.secondaryChildren = secondaryChildren; + } + + public NodeBodyCreate secondaryChildren(List secondaryChildren) { + this.secondaryChildren = secondaryChildren; + return this; + } + + public NodeBodyCreate addSecondaryChildrenItem(ChildAssociationBody secondaryChildrenItem) { + this.secondaryChildren.add(secondaryChildrenItem); + return this; + } + + /** + * Get targets + * @return targets + **/ + @JsonProperty("targets") + public List getTargets() { + return targets; + } + + public void setTargets(List targets) { + this.targets = targets; + } + + public NodeBodyCreate targets(List targets) { + this.targets = targets; + return this; + } + + public NodeBodyCreate addTargetsItem(AssociationBody targetsItem) { + this.targets.add(targetsItem); + return this; + } + + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class NodeBodyCreate {\n"); + + sb.append(" name: ").append(toIndentedString(name)).append("\n"); + sb.append(" nodeType: ").append(toIndentedString(nodeType)).append("\n"); + sb.append(" aspectNames: ").append(toIndentedString(aspectNames)).append("\n"); + sb.append(" properties: ").append(toIndentedString(properties)).append("\n"); + sb.append(" relativePath: ").append(toIndentedString(relativePath)).append("\n"); + sb.append(" association: ").append(toIndentedString(association)).append("\n"); + sb.append(" secondaryChildren: ").append(toIndentedString(secondaryChildren)).append("\n"); + sb.append(" targets: ").append(toIndentedString(targets)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private static String toIndentedString(java.lang.Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} + diff --git a/src/main/java/com/inteligr8/alfresco/bulk/model/NodeBodyCreateAssociation.java b/src/main/java/com/inteligr8/alfresco/bulk/model/NodeBodyCreateAssociation.java new file mode 100644 index 0000000..f69fb36 --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/bulk/model/NodeBodyCreateAssociation.java @@ -0,0 +1,51 @@ +package com.inteligr8.alfresco.bulk.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; + +@com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = true) +public class NodeBodyCreateAssociation { + + @ApiModelProperty(value = "") + private String assocType = null; + /** + * Get assocType + * @return assocType + **/ + @JsonProperty("assocType") + public String getAssocType() { + return assocType; + } + + public void setAssocType(String assocType) { + this.assocType = assocType; + } + + public NodeBodyCreateAssociation assocType(String assocType) { + this.assocType = assocType; + return this; + } + + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class NodeBodyCreateAssociation {\n"); + + sb.append(" assocType: ").append(toIndentedString(assocType)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private static String toIndentedString(java.lang.Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} + diff --git a/src/main/java/com/inteligr8/alfresco/bulk/model/NodeBodyCreateExt.java b/src/main/java/com/inteligr8/alfresco/bulk/model/NodeBodyCreateExt.java new file mode 100644 index 0000000..95458bb --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/bulk/model/NodeBodyCreateExt.java @@ -0,0 +1,49 @@ +package com.inteligr8.alfresco.bulk.model; + +import java.util.LinkedList; +import java.util.List; + +import javax.annotation.concurrent.NotThreadSafe; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@NotThreadSafe +@JsonIgnoreProperties(ignoreUnknown = true) +public class NodeBodyCreateExt extends NodeBodyCreate { + + @JsonProperty + private ContentDataBody contentData; + + @JsonProperty + private List contents; + + public ContentDataBody getContentData() { + return contentData; + } + + public void setContentData(ContentDataBody contentData) { + this.contentData = contentData; + } + + public NodeBodyCreateExt contentData(ContentDataBody contentData) { + this.contentData = contentData; + return this; + } + + public List getContents() { + return contents; + } + + public void setContents(List contents) { + this.contents = contents; + } + + public NodeBodyCreateExt addContent(ContentDataBody contentData) { + if (this.contents == null) + this.contents = new LinkedList<>(); + this.contents.add(contentData); + return this; + } + +} diff --git a/src/main/java/com/inteligr8/alfresco/bulk/model/NodeBodyUpdate.java b/src/main/java/com/inteligr8/alfresco/bulk/model/NodeBodyUpdate.java new file mode 100644 index 0000000..a6992b4 --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/bulk/model/NodeBodyUpdate.java @@ -0,0 +1,154 @@ +package com.inteligr8.alfresco.bulk.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; +import java.util.List; +import java.util.Map; + +@com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = true) +public class NodeBodyUpdate { + + @ApiModelProperty(value = "The name must not contain spaces or the following special characters: * \" < > \\ / ? : and |. The character . must not be used at the end of the name. ") + /** + * The name must not contain spaces or the following special characters: * \" < > \\ / ? : and |. The character . must not be used at the end of the name. + **/ + private String name = null; + + @ApiModelProperty(value = "") + private String nodeType = null; + + @ApiModelProperty(value = "") + private List aspectNames = null; + + @ApiModelProperty(value = "") + private Map properties = null; + + @ApiModelProperty(value = "") + private PermissionsBodyUpdate permissions = null; + /** + * The name must not contain spaces or the following special characters: * \" < > \\ / ? : and |. The character . must not be used at the end of the name. + * @return name + **/ + @JsonProperty("name") + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public NodeBodyUpdate name(String name) { + this.name = name; + return this; + } + + /** + * Get nodeType + * @return nodeType + **/ + @JsonProperty("nodeType") + public String getNodeType() { + return nodeType; + } + + public void setNodeType(String nodeType) { + this.nodeType = nodeType; + } + + public NodeBodyUpdate nodeType(String nodeType) { + this.nodeType = nodeType; + return this; + } + + /** + * Get aspectNames + * @return aspectNames + **/ + @JsonProperty("aspectNames") + public List getAspectNames() { + return aspectNames; + } + + public void setAspectNames(List aspectNames) { + this.aspectNames = aspectNames; + } + + public NodeBodyUpdate aspectNames(List aspectNames) { + this.aspectNames = aspectNames; + return this; + } + + public NodeBodyUpdate addAspectNamesItem(String aspectNamesItem) { + this.aspectNames.add(aspectNamesItem); + return this; + } + + /** + * Get properties + * @return properties + **/ + @JsonProperty("properties") + public Map getProperties() { + return properties; + } + + public void setProperties(Map properties) { + this.properties = properties; + } + + public NodeBodyUpdate properties(Map properties) { + this.properties = properties; + return this; + } + + public NodeBodyUpdate putPropertiesItem(String key, String propertiesItem) { + this.properties.put(key, propertiesItem); + return this; + } + + /** + * Get permissions + * @return permissions + **/ + @JsonProperty("permissions") + public PermissionsBodyUpdate getPermissions() { + return permissions; + } + + public void setPermissions(PermissionsBodyUpdate permissions) { + this.permissions = permissions; + } + + public NodeBodyUpdate permissions(PermissionsBodyUpdate permissions) { + this.permissions = permissions; + return this; + } + + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class NodeBodyUpdate {\n"); + + sb.append(" name: ").append(toIndentedString(name)).append("\n"); + sb.append(" nodeType: ").append(toIndentedString(nodeType)).append("\n"); + sb.append(" aspectNames: ").append(toIndentedString(aspectNames)).append("\n"); + sb.append(" properties: ").append(toIndentedString(properties)).append("\n"); + sb.append(" permissions: ").append(toIndentedString(permissions)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private static String toIndentedString(java.lang.Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} + diff --git a/src/main/java/com/inteligr8/alfresco/bulk/model/NodeBodyUpdateExt.java b/src/main/java/com/inteligr8/alfresco/bulk/model/NodeBodyUpdateExt.java new file mode 100644 index 0000000..ea2fbb4 --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/bulk/model/NodeBodyUpdateExt.java @@ -0,0 +1,99 @@ +package com.inteligr8.alfresco.bulk.model; + +import java.util.LinkedList; +import java.util.List; + +import javax.annotation.concurrent.NotThreadSafe; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@NotThreadSafe +@JsonIgnoreProperties(ignoreUnknown = true) +public class NodeBodyUpdateExt extends NodeBodyUpdate { + + @JsonProperty + private String id; + + @JsonProperty + private ContentDataBody contentData; + + @JsonProperty + private List contents; + + @JsonProperty + private List targets; + + @JsonProperty(defaultValue = "false") + private boolean removeUnspecifiedProperties; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public NodeBodyUpdateExt id(String id) { + this.id = id; + return this; + } + + public ContentDataBody getContentData() { + return contentData; + } + + public void setContentData(ContentDataBody contentData) { + this.contentData = contentData; + } + + public NodeBodyUpdateExt contentData(ContentDataBody contentData) { + this.contentData = contentData; + return this; + } + + public List getContents() { + return contents; + } + + public void setContents(List contents) { + this.contents = contents; + } + + public NodeBodyUpdateExt addContent(ContentDataBody contentData) { + if (this.contents == null) + this.contents = new LinkedList<>(); + this.contents.add(contentData); + return this; + } + + public List getTargets() { + return targets; + } + + public void setTargets(List targets) { + this.targets = targets; + } + + public NodeBodyUpdateExt addTarget(AssociationBody target) { + if (this.targets == null) + this.targets = new LinkedList<>(); + this.targets.add(target); + return this; + } + + public boolean isRemoveUnspecifiedProperties() { + return removeUnspecifiedProperties; + } + + public void setRemoveUnspecifiedProperties(boolean removeUnspecifiedProperties) { + this.removeUnspecifiedProperties = removeUnspecifiedProperties; + } + + public NodeBodyUpdateExt removeUnspecifiedProperties(boolean removeUnspecifiedProperties) { + this.removeUnspecifiedProperties = removeUnspecifiedProperties; + return this; + } + +} diff --git a/src/main/java/com/inteligr8/alfresco/bulk/model/PermissionElement.java b/src/main/java/com/inteligr8/alfresco/bulk/model/PermissionElement.java new file mode 100644 index 0000000..e021b17 --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/bulk/model/PermissionElement.java @@ -0,0 +1,134 @@ +package com.inteligr8.alfresco.bulk.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; +import javax.xml.bind.annotation.XmlEnum; +import javax.xml.bind.annotation.XmlEnumValue; +import javax.xml.bind.annotation.XmlType; + +@com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = true) +public class PermissionElement { + + @ApiModelProperty(value = "") + private String authorityId = null; + + @ApiModelProperty(value = "") + private String name = null; + + +@XmlType(name="AccessStatusEnum") +@XmlEnum(String.class) +public enum AccessStatusEnum { + +@XmlEnumValue("ALLOWED") ALLOWED(String.valueOf("ALLOWED")), @XmlEnumValue("DENIED") DENIED(String.valueOf("DENIED")); + + + private String value; + + AccessStatusEnum (String v) { + value = v; + } + + public String value() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + + public static AccessStatusEnum fromValue(String v) { + for (AccessStatusEnum b : AccessStatusEnum.values()) { + if (String.valueOf(b.value).equals(v)) { + return b; + } + } + return null; + } +} + + @ApiModelProperty(value = "") + private AccessStatusEnum accessStatus = AccessStatusEnum.ALLOWED; + /** + * Get authorityId + * @return authorityId + **/ + @JsonProperty("authorityId") + public String getAuthorityId() { + return authorityId; + } + + public void setAuthorityId(String authorityId) { + this.authorityId = authorityId; + } + + public PermissionElement authorityId(String authorityId) { + this.authorityId = authorityId; + return this; + } + + /** + * Get name + * @return name + **/ + @JsonProperty("name") + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public PermissionElement name(String name) { + this.name = name; + return this; + } + + /** + * Get accessStatus + * @return accessStatus + **/ + @JsonProperty("accessStatus") + public String getAccessStatus() { + if (accessStatus == null) { + return null; + } + return accessStatus.value(); + } + + public void setAccessStatus(AccessStatusEnum accessStatus) { + this.accessStatus = accessStatus; + } + + public PermissionElement accessStatus(AccessStatusEnum accessStatus) { + this.accessStatus = accessStatus; + return this; + } + + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class PermissionElement {\n"); + + sb.append(" authorityId: ").append(toIndentedString(authorityId)).append("\n"); + sb.append(" name: ").append(toIndentedString(name)).append("\n"); + sb.append(" accessStatus: ").append(toIndentedString(accessStatus)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private static String toIndentedString(java.lang.Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} + diff --git a/src/main/java/com/inteligr8/alfresco/bulk/model/PermissionsBodyUpdate.java b/src/main/java/com/inteligr8/alfresco/bulk/model/PermissionsBodyUpdate.java new file mode 100644 index 0000000..3b1b287 --- /dev/null +++ b/src/main/java/com/inteligr8/alfresco/bulk/model/PermissionsBodyUpdate.java @@ -0,0 +1,79 @@ +package com.inteligr8.alfresco.bulk.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; +import java.util.List; + +@com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = true) +public class PermissionsBodyUpdate { + + @ApiModelProperty(value = "") + private Boolean isInheritanceEnabled = null; + + @ApiModelProperty(value = "") + private List locallySet = null; + /** + * Get isInheritanceEnabled + * @return isInheritanceEnabled + **/ + @JsonProperty("isInheritanceEnabled") + public Boolean isIsInheritanceEnabled() { + return isInheritanceEnabled; + } + + public void setIsInheritanceEnabled(Boolean isInheritanceEnabled) { + this.isInheritanceEnabled = isInheritanceEnabled; + } + + public PermissionsBodyUpdate isInheritanceEnabled(Boolean isInheritanceEnabled) { + this.isInheritanceEnabled = isInheritanceEnabled; + return this; + } + + /** + * Get locallySet + * @return locallySet + **/ + @JsonProperty("locallySet") + public List getLocallySet() { + return locallySet; + } + + public void setLocallySet(List locallySet) { + this.locallySet = locallySet; + } + + public PermissionsBodyUpdate locallySet(List locallySet) { + this.locallySet = locallySet; + return this; + } + + public PermissionsBodyUpdate addLocallySetItem(PermissionElement locallySetItem) { + this.locallySet.add(locallySetItem); + return this; + } + + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class PermissionsBodyUpdate {\n"); + + sb.append(" isInheritanceEnabled: ").append(toIndentedString(isInheritanceEnabled)).append("\n"); + sb.append(" locallySet: ").append(toIndentedString(locallySet)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private static String toIndentedString(java.lang.Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} + diff --git a/src/main/resources/alfresco/extension/templates/webscripts/com/inteligr8/alfresco/bulk/createNodes.post.desc.xml b/src/main/resources/alfresco/extension/templates/webscripts/com/inteligr8/alfresco/bulk/createNodes.post.desc.xml new file mode 100644 index 0000000..0798c61 --- /dev/null +++ b/src/main/resources/alfresco/extension/templates/webscripts/com/inteligr8/alfresco/bulk/createNodes.post.desc.xml @@ -0,0 +1,18 @@ + + + Create Nodes + This REST API creates a set of nodes in a single transaction. + It extends the NodeBodyCreate model of the ACS Public REST API for maximum reusability. + The extension allows for creation of associations and in-place content specification.

+

As with any bulk transaction, if this operation fails on any one node, all changes will rollback.

+

TODO

+ ]]>
+ + /inteligr8/bulk/node/{folderNodeId}/nodes + /inteligr8/bulk/nodes/{folderNodeId} + + + user + Inteligr8 Bulk +
diff --git a/src/main/resources/alfresco/extension/templates/webscripts/com/inteligr8/alfresco/bulk/updateNodes.put.desc.xml b/src/main/resources/alfresco/extension/templates/webscripts/com/inteligr8/alfresco/bulk/updateNodes.put.desc.xml new file mode 100644 index 0000000..df2efac --- /dev/null +++ b/src/main/resources/alfresco/extension/templates/webscripts/com/inteligr8/alfresco/bulk/updateNodes.put.desc.xml @@ -0,0 +1,18 @@ + + + Update Nodes + This REST API updates a set of nodes in a single transaction. + It extends the NodeBodyUpdate model of the ACS Public REST API for maximum reusability. + The extension allows for updates to associations and content meta-data. + It also supports the update of node types, regardless of whether the update follows the specialization hierarchy.

+

As with any bulk transaction, if this operation fails on any one node, all changes will rollback.

+

TODO

+ ]]>
+ + /inteligr8/bulk/nodes + + + user + Inteligr8 Bulk +
diff --git a/src/main/resources/alfresco/module/com.inteligr8.alfresco.bulk-acs-module/alfresco-global.properties b/src/main/resources/alfresco/module/com.inteligr8.alfresco.bulk-acs-module/alfresco-global.properties new file mode 100644 index 0000000..f750f0b --- /dev/null +++ b/src/main/resources/alfresco/module/com.inteligr8.alfresco.bulk-acs-module/alfresco-global.properties @@ -0,0 +1,8 @@ + +# Enable to use Tika to synchronously detect the MIME type when MIME type is not provided +# This opens a stream to the binary file and reads at least part of it to help determine the MIME type +# This will slow down imports when the filename extension may be enough to determine the MIME type +inteligr8.bulk.enableTika=false + +# Enable to allow content to be undetermined, which means the MIME type would be 'application/octet-stream'. +inteligr8.bulk.allowOctetStream=false diff --git a/src/main/resources/alfresco/module/com.inteligr8.alfresco.bulk-acs-module/log4j.properties b/src/main/resources/alfresco/module/com.inteligr8.alfresco.bulk-acs-module/log4j.properties new file mode 100644 index 0000000..579fa41 --- /dev/null +++ b/src/main/resources/alfresco/module/com.inteligr8.alfresco.bulk-acs-module/log4j.properties @@ -0,0 +1 @@ +log4j.logger.com.inteligr8.alfresco.bulk=info diff --git a/src/main/resources/alfresco/module/com.inteligr8.alfresco.bulk-acs-module/module-context.xml b/src/main/resources/alfresco/module/com.inteligr8.alfresco.bulk-acs-module/module-context.xml new file mode 100644 index 0000000..29891f2 --- /dev/null +++ b/src/main/resources/alfresco/module/com.inteligr8.alfresco.bulk-acs-module/module-context.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/src/main/resources/alfresco/module/com.inteligr8.alfresco.bulk-acs-module/module.properties b/src/main/resources/alfresco/module/com.inteligr8.alfresco.bulk-acs-module/module.properties new file mode 100644 index 0000000..3f654aa --- /dev/null +++ b/src/main/resources/alfresco/module/com.inteligr8.alfresco.bulk-acs-module/module.properties @@ -0,0 +1,4 @@ +module.id=${project.artifactId} +module.title=${project.name} +module.description=${project.description} +module.version=${project.version} diff --git a/src/test/resources/alfresco/extension/debug-log4j.properties b/src/test/resources/alfresco/extension/debug-log4j.properties new file mode 100644 index 0000000..c2becae --- /dev/null +++ b/src/test/resources/alfresco/extension/debug-log4j.properties @@ -0,0 +1,8 @@ +# Module debugging +log4j.logger.com.inteligr8.alfresco.bulk=trace + +# WebScript debugging +log4j.logger.org.springframework.extensions.webscripts.ScriptLogger=debug + +# non-WebScript JavaScript execution debugging +log4j.logger.org.alfresco.repo.jscript.ScriptLogger=debug diff --git a/src/test/resources/alfresco/extension/disable-webscript-caching-context.xml b/src/test/resources/alfresco/extension/disable-webscript-caching-context.xml new file mode 100644 index 0000000..07829ea --- /dev/null +++ b/src/test/resources/alfresco/extension/disable-webscript-caching-context.xml @@ -0,0 +1,63 @@ + + + + + + + + javascript + + + js + + + + false + + + + + true + + + + + + + + + + ${spaces.store} + + + ${spaces.company_home.childname} + + + + +