package fr.acxio.tools.agia.alfresco; /* * Copyright 2014 Acxio * * 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. */ import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.rmi.RemoteException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.alfresco.webservice.repository.RepositoryServiceSoapBindingStub; import org.alfresco.webservice.repository.UpdateResult; import org.alfresco.webservice.types.CML; import org.alfresco.webservice.types.CMLAddAspect; import org.alfresco.webservice.types.CMLCreate; import org.alfresco.webservice.types.CMLCreateAssociation; import org.alfresco.webservice.types.CMLDelete; import org.alfresco.webservice.types.CMLUpdate; import org.alfresco.webservice.types.CMLWriteContent; import org.alfresco.webservice.types.ContentFormat; import org.alfresco.webservice.types.NamedValue; import org.alfresco.webservice.types.ParentReference; import org.alfresco.webservice.types.Predicate; import org.alfresco.webservice.types.Query; import org.alfresco.webservice.types.Reference; import org.alfresco.webservice.util.Constants; import org.alfresco.webservice.util.ContentUtils; import org.alfresco.webservice.util.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.batch.item.ItemWriter; import fr.acxio.tools.agia.alfresco.domain.Aspect; import fr.acxio.tools.agia.alfresco.domain.Association; import fr.acxio.tools.agia.alfresco.domain.Document; import fr.acxio.tools.agia.alfresco.domain.Folder; import fr.acxio.tools.agia.alfresco.domain.Node; import fr.acxio.tools.agia.alfresco.domain.NodeList; import fr.acxio.tools.agia.alfresco.domain.Property; import fr.acxio.tools.agia.alfresco.domain.QName; import fr.acxio.tools.agia.alfresco.domain.QueryAssociation; import fr.acxio.tools.agia.alfresco.domain.RefAssociation; /** * <p> * A advanced {@link org.springframework.batch.item.ItemWriter ItemWriter} for * Alfresco that can write {@link fr.acxio.tools.agia.alfresco.domain.NodeList * NodeList} to an Alfresco instance. * </p> * * <p> * Stateless, so restartable. * </p> * * <p> * WARNING: using an async executor may induce race condition on folders * creation because one can be created in a first transaction when the same one * is also created in a second transaction. The first transaction may succeed, * and then the second one will fail because the folder already exists. * </p> * * <p> * If an object is set to be replaced, a first transaction is executed to delete * it, then a second one is executed to create it.</br> If the second * transaction fails, the deleted objects are not restored. * </p> * * <p> * If a document does not have the versionable aspect but is set to be * versioned, the new version will overwrite the current one and it will have * the versionable aspect so that next version will not overwrite it. * </p> * * <p> * The contents are send like properties into the transaction. * </p> * * @author pcollardez * */ public class AlfrescoNodeWriter extends AlfrescoServicesConsumer implements ItemWriter<NodeList> { // WARNING : using an Async Executor may induce race condition on folders // creation // Some documents may be created, some other may not (probably a pb with the // cache) // Since a nice fix is done, use a SyncTaskExecutor ! private static final Logger LOGGER = LoggerFactory.getLogger(AlfrescoNodeWriter.class); private boolean sendContents = true; public void setSendContents(boolean sSendContents) { sendContents = sSendContents; } @Override public void write(List<? extends NodeList> sData) throws NodePathException, VersionOperationException, IOException { if (!sData.isEmpty()) { init(); RepositoryServiceSoapBindingStub repositoryService = getAlfrescoService().getRepositoryService(); CMLHelper aCMLHelper = new CMLHelper(); // Prepare only one // transaction // Maps ID of each node and References used by associations Map<Node, Integer> aNodesIndexes = buildNodesIndexes(sData); String aCurrentNodePath; Map<String, List<Integer>> aNodesRefIndexes; for (NodeList aNodeList : sData) { // each NodeList represents an // input record aNodesRefIndexes = buildNodesRefIndexes(sData, aNodesIndexes, aNodeList); for (Node aNode : aNodeList) { aCurrentNodePath = aNode.getPath(); // 1. L'objet n'est pas dans Alf et donc pas dans le cache // => création en toute circonstance // 2. L'object est dans Alf mais pas dans le cache => cache // mis à jour, opération quelconque // 3. L'object est dans Alf et dans le cache => on a besoin // d'un cache dans le context de la transaction pour // réutiliser le même objet CML // 4. L'object n'est plus dans Alf mais est dans le cache => // on aura une erreur au commit, auquel cas il faut purger // le cache et recommencer // et si on a une 2eme erreur, alors l'opération est // réellement en erreur if (!aCMLHelper.isPathExist(aCurrentNodePath)) { // If the // path // already // exists // in the // current // transaction, // dont add // the node // another // time createOrUpdateNode(repositoryService, aCMLHelper, aNodesIndexes, aNodesRefIndexes, aCurrentNodePath, aNode); aCMLHelper.addExistingPath(aCurrentNodePath); } } } if (LOGGER.isDebugEnabled()) { LOGGER.debug("Will commit"); } UpdateResult[] result = repositoryService.update(aCMLHelper.getCML()); // Update the cache of paths and references updateDataWithResult(sData, aNodesIndexes, result); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Commited"); } cleanup(); } } protected void createOrUpdateNode(RepositoryServiceSoapBindingStub sRepositoryService, CMLHelper sCMLHelper, Map<Node, Integer> sNodesIndexes, Map<String, List<Integer>> sNodesRefIndexes, String sCurrentNodePath, Node sNode) throws NodePathException, VersionOperationException, IOException, RemoteException { org.alfresco.webservice.types.Node[] nodes = getRepositoryMatchingNodes(sRepositoryService, sCurrentNodePath); if ((nodes != null) && (nodes.length > 0)) { if (nodes.length > 1) { throw new VersionOperationException("Too many matching nodes"); } org.alfresco.webservice.types.Node aRepositoryNode = nodes[0]; Predicate sWhereNode = new Predicate(new Reference[] { aRepositoryNode.getReference() }, null, null); // If this is a folder, just ignore it or update props ? // If this is a document, replace, version or leave it as it is if (sNode instanceof Document) { updateDocument(sRepositoryService, sCMLHelper, sNodesIndexes, sCurrentNodePath, sNodesRefIndexes, sNode, sWhereNode); } else if (sNode instanceof Folder) { updateFolder(sRepositoryService, sCMLHelper, sNodesIndexes, sCurrentNodePath, sNodesRefIndexes, sNode, sWhereNode); } } else { createNewNode(sRepositoryService, sCMLHelper, sNodesIndexes, sCurrentNodePath, sNodesRefIndexes, sNode); } } protected void updateDataWithResult(List<? extends NodeList> sData, Map<Node, Integer> sNodesIndexes, UpdateResult[] sResult) throws NodePathException { Map<String, Node> aIndexes = buildIndexesNodesRef(sNodesIndexes); for (UpdateResult aUpdateResult : sResult) { Reference aDestination = aUpdateResult.getDestination(); if (aDestination != null) { Reference aSource = aUpdateResult.getSource(); if (aUpdateResult.getSourceId() != null) { Node aNode = aIndexes.get(aUpdateResult.getSourceId()); if (aNode != null) { // If the source is null, it is a "create" statement, // and the node reference is in the destination aNode.setScheme((aSource != null) ? aSource.getStore().getScheme() : aDestination.getStore().getScheme()); aNode.setAddress((aSource != null) ? aSource.getStore().getAddress() : aDestination.getStore().getAddress()); aNode.setUuid((aSource != null) ? aSource.getUuid() : aDestination.getUuid()); } } for (NodeList aNodeList : sData) { for (Node aNode : aNodeList) { // TODO : cache the getPath result if ((aUpdateResult.getSourceId() == null) && (aSource != null) && aNode.getPath().equals(aSource.getPath())) { aNode.setScheme(aSource.getStore().getScheme()); aNode.setAddress(aSource.getStore().getAddress()); aNode.setUuid(aSource.getUuid()); } if (aNode.getPath().equals(aDestination.getPath())) { aNode.setScheme(aDestination.getStore().getScheme()); aNode.setAddress(aDestination.getStore().getAddress()); aNode.setUuid(aDestination.getUuid()); } } } org.alfresco.webservice.types.Node aNewRepositoryNode = new org.alfresco.webservice.types.Node(); aNewRepositoryNode.setReference(aDestination); setLocalMatchingNodes(aDestination.getPath(), new org.alfresco.webservice.types.Node[] { aNewRepositoryNode }); } } } protected void createNewNode(RepositoryServiceSoapBindingStub sRepositoryService, CMLHelper sCMLHelper, Map<Node, Integer> sNodesIndexes, String sCurrentNodePath, Map<String, List<Integer>> sNodesRefIndexes, Node sNode) throws IOException { // Create a new node if (LOGGER.isDebugEnabled()) { LOGGER.debug("Will add node " + sNode.getId()); } addCreateForNode(sCMLHelper, sNode, sNodesIndexes.get(sNode)); addAssociationsForNode(sCMLHelper, sNode, null, sNodesRefIndexes, sNodesIndexes.get(sNode)); if (sNode instanceof Document) { if (sNode.getVersionOperation().equals(Node.VersionOperation.VERSION)) { sCMLHelper.addAddAspect(new CMLAddAspect(Constants.ASPECT_VERSIONABLE, null, null, sNodesIndexes.get(sNode).toString())); } addContentForNode(sCMLHelper, (Document) sNode, null, sNodesIndexes.get(sNode)); } // Clear the cache for the current node to force reload it evictRepositoryNode(sCurrentNodePath); } protected void updateFolder(RepositoryServiceSoapBindingStub sRepositoryService, CMLHelper sCMLHelper, Map<Node, Integer> sNodesIndexes, String sCurrentNodePath, Map<String, List<Integer>> sNodesRefIndexes, Node sNode, Predicate sWhereNode) throws RemoteException { // Node.VersionOperation.RAISEERROR : Do nothing if (sNode.getVersionOperation().equals(Node.VersionOperation.REPLACE)) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Will replace folder node " + sNode.getId()); } // WARNING : The node is deleted OUT OF THE CURRENT TRANSACTION !!! // But they are sent to the deleted items of the user used to // connect to Alfresco // FIXME : will fail if the folder is not empty CML aCML = new CML(); aCML.setDelete(new CMLDelete[] { new CMLDelete(sWhereNode) }); sRepositoryService.update(aCML); addCreateForNode(sCMLHelper, sNode, sNodesIndexes.get(sNode)); addAssociationsForNode(sCMLHelper, sNode, null, sNodesRefIndexes, sNodesIndexes.get(sNode)); // Clear the cache for the current node to force reload it evictRepositoryNode(sCurrentNodePath); } else if (sNode.getVersionOperation().equals(Node.VersionOperation.UPDATE) || sNode.getVersionOperation().equals(Node.VersionOperation.VERSION)) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Will skip folder node " + sNode.getId()); } // Folders are not versionable, just update them sCMLHelper.addUpdate(new CMLUpdate(getProperties(sNode), sWhereNode, null)); addAspectsForNode(sCMLHelper, sNode, sWhereNode, 0); addAssociationsForNode(sCMLHelper, sNode, sWhereNode, sNodesRefIndexes, 0); // Clear the cache for the current node to force reload it evictRepositoryNode(sCurrentNodePath); } } protected void updateDocument(RepositoryServiceSoapBindingStub sRepositoryService, CMLHelper sCMLHelper, Map<Node, Integer> sNodesIndexes, String sCurrentNodePath, Map<String, List<Integer>> sNodesRefIndexes, Node sNode, Predicate sWhereNode) throws VersionOperationException, IOException { if (sNode.getVersionOperation().equals(Node.VersionOperation.RAISEERROR)) { throw new VersionOperationException("Node already exists " + sNode.getId()); } else if (sNode.getVersionOperation().equals(Node.VersionOperation.REPLACE)) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Will replace document node " + sNode.getId()); } // aCMLHelper.addDelete(new CMLDelete(sWhereNode)); // THIS DOES NOT // WORK because every CMLCreate happen BEFORE every CMLDelete // WARNING : The node is deleted OUT OF THE CURRENT TRANSACTION !!! // But they are sent to the deleted items of the user used to // connect to Alfresco CML aCML = new CML(); aCML.setDelete(new CMLDelete[] { new CMLDelete(sWhereNode) }); sRepositoryService.update(aCML); addCreateForNode(sCMLHelper, sNode, sNodesIndexes.get(sNode)); addAssociationsForNode(sCMLHelper, sNode, null, sNodesRefIndexes, sNodesIndexes.get(sNode)); addContentForNode(sCMLHelper, (Document) sNode, null, sNodesIndexes.get(sNode)); } else if (sNode.getVersionOperation().equals(Node.VersionOperation.UPDATE)) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Will update document node " + sNode.getId()); } // WARNING : Updating a versionable node will create a new version sCMLHelper.addUpdate(new CMLUpdate(getProperties(sNode), sWhereNode, null)); addAspectsForNode(sCMLHelper, sNode, sWhereNode, 0); addAssociationsForNode(sCMLHelper, sNode, sWhereNode, sNodesRefIndexes, 0); addContentForNode(sCMLHelper, (Document) sNode, sWhereNode, 0); } else if (sNode.getVersionOperation().equals(Node.VersionOperation.VERSION)) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Will create a new version of document node " + sNode.getId()); } // WARNING : the update will not create a version if there is not // property or content change // WARNING : the aspect is added AFTER the properties or content // change // so the document will not retain its current version and the new // one will update it sCMLHelper.addAddAspect(new CMLAddAspect(Constants.ASPECT_VERSIONABLE, null, sWhereNode, null)); sCMLHelper.addUpdate(new CMLUpdate(getProperties(sNode), sWhereNode, null)); addAspectsForNode(sCMLHelper, sNode, sWhereNode, 0); addAssociationsForNode(sCMLHelper, sNode, sWhereNode, sNodesRefIndexes, 0); addContentForNode(sCMLHelper, (Document) sNode, sWhereNode, 0); } // Clear the cache for the current node to force reload it evictRepositoryNode(sCurrentNodePath); } protected Map<String, Node> buildIndexesNodesRef(Map<Node, Integer> sNodesRefIndexes) { Map<String, Node> aIndexesNodesRef = null; if (sNodesRefIndexes != null) { aIndexesNodesRef = new HashMap<String, Node>(sNodesRefIndexes.size()); for (Map.Entry<Node, Integer> aEntry : sNodesRefIndexes.entrySet()) { aIndexesNodesRef.put(aEntry.getValue().toString(), aEntry.getKey()); } } return aIndexesNodesRef; } protected Map<String, List<Integer>> buildNodesRefIndexes(List<? extends NodeList> sData, Map<Node, Integer> sNodesIndexes, NodeList sNodeList) { Map<String, List<Integer>> aNodesRefIndexes; List<Integer> aTargetIds; aNodesRefIndexes = new HashMap<String, List<Integer>>(sData.size()); for (Node aNode : sNodeList) { if ((aNode.getAssocTargetId() != null) && !aNode.getAssocTargetId().isEmpty()) { aTargetIds = aNodesRefIndexes.get(aNode.getAssocTargetId()); if (aTargetIds == null) { aTargetIds = new ArrayList<Integer>(1); } aTargetIds.add(sNodesIndexes.get(aNode)); aNodesRefIndexes.put(aNode.getAssocTargetId(), aTargetIds); } } return aNodesRefIndexes; } protected Map<Node, Integer> buildNodesIndexes(List<? extends NodeList> sData) { int aNodeIndex = 0; Map<Node, Integer> aNodesIndexes = new HashMap<Node, Integer>(sData.size()); for (NodeList aNodeList : sData) { for (Node aNode : aNodeList) { if (!aNodesIndexes.containsKey(aNode)) { aNodesIndexes.put(aNode, aNodeIndex); aNodeIndex++; } } } return aNodesIndexes; } private NamedValue[] getProperties(Node sNode) { List<Property> aProperties = sNode.getProperties(); NamedValue[] aProps = new NamedValue[aProperties.size()]; int i = 0; List<String> aPropValues; for (Property aProperty : aProperties) { aPropValues = aProperty.getValues(); if (aPropValues.size() > 1) { String[] aValues = aPropValues.toArray(new String[] {}); aProps[i] = Utils.createNamedValue(aProperty.getName().toString(), aValues); } else { // FIXME : if the property has no value, it is different than // having a value set to null String aValue = (aPropValues.size() == 0) ? null : aPropValues.get(0); aProps[i] = Utils.createNamedValue(aProperty.getName().toString(), aValue); } i++; } return aProps; } private void addCreateForNode(CMLHelper sCMLHelper, Node sNode, int sNodeIndex) { NamedValue[] aProps = getProperties(sNode); ParentReference aParentReference = new ParentReference(STORE, null, sNode.getParent().getPath(), Constants.ASSOC_CONTAINS, new QName(sNode.getType() .getNamespaceURI(), sNode.getName()).toString()); String aNodeType = (sNode.getType() == null) ? Constants.TYPE_CONTENT : sNode.getType().toString(); sCMLHelper.addCreate(new CMLCreate(Integer.toString(sNodeIndex), aParentReference, null, null, null, aNodeType, aProps)); addAspectsForNode(sCMLHelper, sNode, null, sNodeIndex); } private void addAspectsForNode(CMLHelper sCMLHelper, Node sNode, Predicate sPredicate, int sNodeIndex) { List<Aspect> aAspectsList = sNode.getAspects(); if (aAspectsList.size() > 0) { for (Aspect aAspect : aAspectsList) { sCMLHelper.addAddAspect(new CMLAddAspect(aAspect.getName().toString(), null, sPredicate, ((sPredicate != null) ? null : Integer .toString(sNodeIndex)))); } } } private void addContentForNode(CMLHelper sCMLHelper, Document sDocument, Predicate sPredicate, int sNodeIndex) throws IOException { if (sendContents && (sDocument.getContentPath() != null) && (sDocument.getContentPath().length() > 0)) { // If the document has a content path, the file must exist File aFile = new File(sDocument.getContentPath()); if (aFile.exists() && aFile.isFile()) { FileInputStream aInputStream = new FileInputStream(aFile); try { sCMLHelper.addWriteContent(new CMLWriteContent(Constants.PROP_CONTENT, ContentUtils.convertToByteArray(aInputStream), new ContentFormat( sDocument.getMimeType(), sDocument.getEncoding()), sPredicate, ((sPredicate != null) ? null : Integer.toString(sNodeIndex)))); } catch (Exception e) { throw new IOException("Cannot convert and write content", e); } } else { throw new IOException("Cannot find content file: " + sDocument.getContentPath()); } } } private void addAssociationsForNode(CMLHelper sCMHelper, Node sNode, Predicate sPredicate, Map<String, List<Integer>> sNodesRefIndexes, int sNodeIndex) { List<Association> aAssociationList = sNode.getAssociations(); if (aAssociationList.size() > 0) { for (Association aAssociation : aAssociationList) { if (aAssociation instanceof RefAssociation) { List<Integer> aTargetIndex = sNodesRefIndexes.get(((RefAssociation) aAssociation).getReference()); if (aTargetIndex != null) { for (Integer aIndex : aTargetIndex) { sCMHelper.addCreateAssociation(new CMLCreateAssociation(sPredicate, ((sPredicate != null) ? null : Integer.toString(sNodeIndex)), null, aIndex.toString(), aAssociation.getType().toString())); } } } else if (aAssociation instanceof QueryAssociation) { Query aQuery = new Query(((QueryAssociation) aAssociation).getQueryLanguage(), ((QueryAssociation) aAssociation).getQuery()); Predicate aQueryPredicate = new Predicate(null, STORE, aQuery); sCMHelper.addCreateAssociation(new CMLCreateAssociation(sPredicate, ((sPredicate != null) ? null : Integer.toString(sNodeIndex)), aQueryPredicate, null, aAssociation.getType().toString())); } } } } }