/*
* Copyright (C) 2008-2012 Open Wide SA
*
* This program is FREE software. You can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your opinion) 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, write to the Free Software Foundation,
* Inc. ,59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*
* More information at http://knowledge.openwide.fr/bin/view/Main/AlfrescoETLConnector/
*
*/
package fr.openwide.talendalfresco.alfresco.importer;
import java.io.Reader;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.transaction.UserTransaction;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.importer.ImportNode;
import org.alfresco.repo.importer.Importer;
import org.alfresco.repo.importer.view.NodeContext;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentData;
import org.alfresco.service.cmr.repository.MimetypeService;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.view.ImportPackageHandler;
import org.alfresco.service.cmr.view.ImporterBinding;
import org.alfresco.service.cmr.view.ImporterException;
import org.alfresco.service.cmr.view.ImporterProgress;
import org.alfresco.service.cmr.view.Location;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.ParameterCheck;
import org.alfresco.web.app.Application;
import org.alfresco.web.bean.repository.Repository;
import fr.openwide.talendalfresco.acpxml.AcpXmlUtil;
import fr.openwide.talendalfresco.alfresco.NamePathService;
import fr.openwide.talendalfresco.alfresco.util.ContentDataUtil;
import fr.openwide.talendalfresco.importer.ContentImporterConfiguration;
/**
* Imports content (cm:object).
* Extended features beyond the parent's :
* * allows to use namepath (with company home as its root) in addition to xpath to specify targetLocation (binding.location)
* * missing containers in targetLocation (binding.location) are created
* * wraps every node import (more precisely NodeImporter.importNode(), i.e. node creation,
* aspects, properties, linking, content : everything but containment associations) in its own
* transaction
* * each node import & transaction can be logged with detailed info
* * missing content mimetype is guessed from the content url
*
* @author Marc Dutoo - Open Wide SA
*
*/
public class ContentImporterComponent extends ContentImporterComponentBase {
protected static final QName DATATYPE_CONTENT = QName.createQName(NamespaceService.DICTIONARY_MODEL_1_0_URI, "content");
/** to wrap node imports with txns */
protected TransactionService transactionService;
/** to guess content mimetype */
protected MimetypeService mimetypeService;
/** to find existing nodes using namePathes */
protected NamePathService namePathService;
/**
* Get the target location nodeRef using a quick xpath (db) search.
* In addition to parent : create it if it does not exist, allow namePath.
*/
@Override
protected NodeRef getNodeRef(Location location, ImporterBinding binding) {
// can't use super, throw undiferrenciated exceptions
//NodeRef targetLocationNodeRef = super.getNodeRef(location, binding);
// see super
ParameterCheck.mandatory("Location", location);
// Establish node to import within
NodeRef nodeRef = location.getNodeRef();
if (nodeRef == null)
{
// If a specific node has not been provided, default to the root
nodeRef = nodeService.getRootNode(location.getStoreRef());
}
// Resolve to path within node, if one specified
String path = location.getPath();
if (path != null) {
// [talendalfresco] allowing empty path i.e. content root
// Create a valid path and search
path = bindPlaceHolder(path, binding);
if (path.indexOf(":") == -1) { // TODO always
// [talendalfresco] not an xpath ; assuming namepath syntax
nodeRef = getOrCreateTargetContainer(path, (ContentImporterBinding) binding);
if (nodeRef == null) {
throw new ImporterException("Name path " + path + " does not exist and its creation failed, aborting import.");
}
} else {
// [talendalfresco] xpath syntax : allowing original behaviour
path = createValidPath(path);
List<NodeRef> nodeRefs = searchService.selectNodes(nodeRef, path, null, namespaceService, false);
if (nodeRefs.size() == 0) {
throw new ImporterException("Path " + path + " within node " + nodeRef + " does not exist - the path must resolve to a valid location "
+ "when using xpath syntax, or be provided in namepath syntax.");
} else if (nodeRefs.size() > 1) {
throw new ImporterException("Path " + path + " within node " + nodeRef + " found too many locations - the path must resolve to one location");
} else {
nodeRef = nodeRefs.get(0);
}
}
}
return nodeRef;
}
/**
* Imports contents, therefore below the companyHome,
* therefore a namepath should never start with /Alfresco
* (namePath version)
* TODO txn or in sub
* @param path a namepath with the company home as its root
* @param contentImporterBinding
* @return null if failed (errors have been logged in parallel)
*/
protected NodeRef getOrCreateTargetContainer(String namePath,
ContentImporterBinding contentImporterBinding) {
// getting company home as the root of the path :
NodeRef companyHomeNodeRef = new NodeRef(Repository.getStoreRef(), Application.getCompanyRootId());
// NB. can't use store root as the root of the path, because it does not use cm:contains
NodeRef currentNodeRef = companyHomeNodeRef;
ArrayList<String> pathNames = AcpXmlUtil.stringToNamePath(namePath,
contentImporterBinding.clientPathDelimiter); // better than StringUtils and split()
for (String pathName : pathNames) {
NodeRef foundOrCreatedNodeRef = namePathService.getChildByPathName(pathName, currentNodeRef,
contentImporterBinding.targetLocationChildAssociationTypeQName);
if (foundOrCreatedNodeRef == null) {
// if update only mode, this is an error
if (ContentImporterConfiguration.CONTAINER_MODE_UPDATE_ONLY.equals(contentImporterBinding.containerMode)) {
contentImporterBinding.contentImporterResultHandler.containerError(namePath, companyHomeNodeRef,
new Exception("Container at path " + namePath
+ " doesn't exist and can't be created in container mode "
+ contentImporterBinding.containerMode));
return null;
}
// else create the container :
UserTransaction userTransaction = null;
try {
long start = System.nanoTime();
// get transaction : not getNonPropagatingUserTransaction which creates zombie nodes !!
userTransaction = transactionService.getUserTransaction();
userTransaction.begin();
foundOrCreatedNodeRef = this.createChildFolder(currentNodeRef, pathName, contentImporterBinding);
userTransaction.commit();
if (!nodeService.exists(foundOrCreatedNodeRef)) {
// commit ends well but node not created because it did some underlaying non propagated transactions of its own
logger.error("ContentImporterComponent : zombie noderef " + foundOrCreatedNodeRef + " created (container " + pathName + ")");
}
long end = System.nanoTime();
// success handling
// TODO log mode
contentImporterBinding.contentImporterResultHandler.containerSuccess(
foundOrCreatedNodeRef, "in " + (end - start) + "ns"); // TODO context
} catch (Throwable t) {
try {
if (userTransaction != null) {
userTransaction.rollback();
}
} catch (Exception ex) {
logger.debug("Unknown exception while rollbacking", ex);
}
// error handling
contentImporterBinding.contentImporterResultHandler.containerError(pathName, currentNodeRef, t);
return null;
}
}
// looping
currentNodeRef = foundOrCreatedNodeRef;
}
return currentNodeRef;
}
/**
* (namePath version)
* Gets the existing subcontainer with the given name,
* or create it if it doesn't exist yet.
* TODO rights, mode, txns
* @param parentContainerNodeRef
* @param childContainerName
* @param contentImporterBinding
* @return
*/
protected NodeRef createChildFolder(NodeRef parentContainerNodeRef, String childContainerName,
ContentImporterBinding contentImporterBinding) {
// building properties : only name
Map<QName, Serializable> childFolderProperties = new HashMap<QName, Serializable>(1);
childFolderProperties.put(ContentModel.PROP_NAME, childContainerName);
// creating childName from name
QName childAssociationQName = QName.createQName(contentImporterBinding.targetLocationContainerTypeQName.getNamespaceURI(),
QName.createValidLocalName(childContainerName)); // TODO é ??
// creating node and association
ChildAssociationRef childFolderAssoc = nodeService.createNode(parentContainerNodeRef,
contentImporterBinding.targetLocationChildAssociationTypeQName, childAssociationQName,
contentImporterBinding.targetLocationContainerTypeQName, childFolderProperties);
return childFolderAssoc.getChildRef();
}
/**
* [talendalfresco] uses custom NodeImporter
* Perform Import via Parser
*
* @param nodeRef node reference to import under
* @param childAssocType the child association type to import under
* @param inputStream the input stream to import from
* @param streamHandler the content property import stream handler
* @param binding import configuration
* @param progress import progress
*/
public void parserImport(NodeRef nodeRef, QName childAssocType, Reader viewReader, ImportPackageHandler streamHandler, ImporterBinding binding, ImporterProgress progress)
{
//ParameterCheck.mandatory("Node Reference", nodeRef);
if (nodeRef == null) return; // [talendalfresco] fails because of previous errors (getOrCreateContainer or importNode)
ParameterCheck.mandatory("View Reader", viewReader);
ParameterCheck.mandatory("Stream Handler", streamHandler);
Importer nodeImporter = new ContentNodeImporter(nodeRef, childAssocType, binding, streamHandler, progress); // [talendalfresco]
try
{
nodeImporter.start();
viewParser.parse(viewReader, nodeImporter);
nodeImporter.end();
}
catch(RuntimeException e)
{
logger.warn("Import error", e);
nodeImporter.error(e);
throw e;
}
}
/**
* TODO handle modes, ex. by overriding createNodeImporterStrategy
* @author mdutoo
*
*/
protected class ContentNodeImporter extends NodeImporter {
/** to ease non-casted access to our contentImporterBinding */
protected ContentImporterBinding contentImporterBinding;
/** to ease access to our result handler */
protected ContentImporterResultHandler contentImporterResultHandler;
public ContentImporterResultHandler getContentImporterResultHandler() {
return contentImporterResultHandler;
}
protected ContentNodeImporter(NodeRef rootRef, QName rootAssocType,
ImporterBinding binding, ImportPackageHandler streamHandler,
ImporterProgress progress) {
super(rootRef, rootAssocType, binding, streamHandler, progress);
this.contentImporterBinding = (ContentImporterBinding) binding;
this.contentImporterResultHandler = this.contentImporterBinding.contentImporterResultHandler;
// specific import strategy allowing to handle modes :
this.importStrategy = new ContainerDocumentCreationModeWrappingImportStrategy(
new CreateNewNodeImporterStrategy(false), this.updateStrategy);
}
/**
* Need to override to handle node import failure
*/
public NodeRef importNode(ImportNode context) {
if (context.getParentContext().getParentRef() == null) {
// import of parent already failed, so can't import this node
return null;
}
NodeRef importedNodeRef = null;
UserTransaction userTransaction = null;
try {
// import it in a transaction
long start = System.nanoTime();
// get transaction : not getNonPropagatingUserTransaction which creates zombie nodes !!
userTransaction = transactionService.getUserTransaction();
userTransaction.begin();
importedNodeRef = super.importNode(context);
userTransaction.commit();
if (!nodeService.exists(importedNodeRef)) {
// commit ends well but node not created because it did some underlaying non propagated transactions of its own
logger.error("ContentImporterComponent : zombie noderef " + importedNodeRef + " created (" + context.getProperties() + ")");
}
long end = System.nanoTime();
// finally success handling
if (importedNodeRef != null) {
// TODO log mode
this.contentImporterResultHandler.nodeSuccess(context, importedNodeRef, "in " + (end - start) + "ns");
} // else { // import of parent failed beforehand
// this.contentImporterResultHandler.nodeError(context, new Exception("Failed because parent doesn't exist"));
//}
} catch (Throwable t) {
// node import has failed
// let's rollback, log errors and skip over to the next node
try {
if (userTransaction != null) {
userTransaction.rollback();
}
} catch (Exception ex) {
logger.debug("Unknown exception while rollbacking", ex);
}
// error handling
this.contentImporterResultHandler.nodeError(context, t);
// NB. good error handling besides here : in ViewParser.importNode(), non created node is not registered as importId OK !
}
return importedNodeRef;
}
/**
* [talendalfresco] impl'd on the super method, but adds before
* guessed mimetype if none yet.
* NB. adds an unnecessary bindPlaceHolder(), but not an issue
*/
protected void importContent(NodeRef nodeRef, QName propertyName, String importContentData) {
// bind import content data description
// [talendalfresco] duplicated with super but not an issue
importContentData = bindPlaceHolder(importContentData, binding);
if (importContentData != null && importContentData.length() > 0) {
// guessing mimetype if none yet :
// NB. done before conversion to ContentData, else conversion fails on missing mimetype
ContentData contentDataWithGuessedMimetype =
ContentDataUtil.createContentPropertyWithGuessedMimetype(importContentData, mimetypeService);
super.importContent(nodeRef, propertyName, contentDataWithGuessedMimetype.toString());
}
}
/**
* Handles modes
* @author mdutoo
*
*/
protected class ContainerDocumentCreationModeWrappingImportStrategy implements NodeImporterStrategy {
/** create new node, explode (because of name constraint) if one already exists with the same name */
protected NodeImporterStrategy createImportStrategy;
/** if a node with the context's UUID exists, update it, else do the create strategy */
protected NodeImporterStrategy createOrUpdateImportStrategy;
/**
*
* @param createImportStrategy create new node, explode
* (because of name constraint) if one already exists with the same name
* @param createOrUpdateImportStrategy if a node with the
* context's UUID exists, update it, else do the create strategy
*/
public ContainerDocumentCreationModeWrappingImportStrategy(
CreateNewNodeImporterStrategy createImportStrategy,
UpdateExistingNodeImporterStrategy createOrUpdateImportStrategy) {
this.createImportStrategy = createImportStrategy;
this.createOrUpdateImportStrategy = createOrUpdateImportStrategy;
}
public NodeRef importNode(ImportNode context) {
NodeImporterStrategy importStrategy; // to be used
String childName = context.getChildName();
if (ContentImporterConfiguration.CHILD_NAME_CONTAINER.equals(childName)) {
// container mode
String name = (String) context.getProperties().get(ContentModel.PROP_NAME);
NodeRef existingNodeRef = namePathService.getChildByPathName(name,
context.getParentContext().getParentRef(),
ContentModel.ASSOC_CONTAINS);
if (ContentImporterConfiguration.CONTAINER_MODE_CREATE_OR_UPDATE.equals(contentImporterBinding.containerMode)) {
if (existingNodeRef != null) {
((NodeContext) context).setUUID(existingNodeRef.getId());
}
// if a node with the context's UUID exists, update it, else do the create strategy
importStrategy = this.createOrUpdateImportStrategy;
} else { // ContentImporterConfiguration.CONTAINER_MODE_UPDATE_ONLY
if (existingNodeRef != null) {
((NodeContext) context).setUUID(existingNodeRef.getId());
} else {
throw new AlfrescoRuntimeException("Trying to import container in mode "
+ ContentImporterConfiguration.CONTAINER_MODE_UPDATE_ONLY
+ ", but node does not exists with name " + name);
}
importStrategy = this.createOrUpdateImportStrategy;
}
} else if (ContentImporterConfiguration.CHILD_NAME_DOCUMENT.equals(childName)) {
// document mode
String name = (String) context.getProperties().get(ContentModel.PROP_NAME);
NodeRef existingNodeRef = namePathService.getChildByPathName(name,
context.getParentContext().getParentRef(),
ContentModel.ASSOC_CONTAINS);
if (ContentImporterConfiguration.DOCUMENT_MODE_CREATE_ONLY.equals(contentImporterBinding.documentMode)) {
if (existingNodeRef != null) {
throw new AlfrescoRuntimeException("Trying to import document in mode "
+ ContentImporterConfiguration.DOCUMENT_MODE_CREATE_ONLY
+ ", but node already exists with name " + name + " and noderef " + existingNodeRef);
}
// create new node, explode (because of name constraint) if one already exists with the same name
importStrategy = this.createImportStrategy;
} else { // if (ContentImporterConfiguration.DOCUMENT_MODE_CREATE_OR_UPDATE
if (existingNodeRef != null) {
((NodeContext) context).setUUID(existingNodeRef.getId());
}
// if a node with the context's UUID exists, update it, else do the create strategy
importStrategy = this.createOrUpdateImportStrategy;
}
} else {
// old behaviour is preserved
importStrategy = this.createImportStrategy;
}
return importStrategy.importNode(context);
}
}
/**
* [talendalfresco] overriden to transactionalize reference resolving
* (non-Javadoc)
* @see org.alfresco.repo.importer.Importer#end()
*/
@SuppressWarnings("unchecked")
public void end()
{
// Bind all node references to destination space
for (ImportedNodeRef importedRef : nodeRefs)
{
// [talendalfresco] starting transaction
UserTransaction userTransaction = null;
try {
long start = System.nanoTime();
// get transaction : not getNonPropagatingUserTransaction which creates zombie nodes !!
userTransaction = transactionService.getUserTransaction();
userTransaction.begin();
Serializable refProperty = null;
if (importedRef.value != null)
{
if (importedRef.value instanceof Collection)
{
Collection<String> unresolvedRefs = (Collection<String>)importedRef.value;
List<NodeRef> resolvedRefs = new ArrayList<NodeRef>(unresolvedRefs.size());
for (String unresolvedRef : unresolvedRefs)
{
if (unresolvedRef != null)
{
NodeRef nodeRef = resolveImportedNodeRef(importedRef.context.getNodeRef(), unresolvedRef);
// TODO: Provide a better mechanism for invalid references? e.g. report warning
if (nodeRef != null)
{
resolvedRefs.add(nodeRef);
}
}
}
refProperty = (Serializable)resolvedRefs;
}
else
{
refProperty = resolveImportedNodeRef(importedRef.context.getNodeRef(), (String)importedRef.value);
// TODO: Provide a better mechanism for invalid references? e.g. report warning
}
}
// Set node reference on source node
Set<QName> disabledBehaviours = getDisabledBehaviours(importedRef.context);
try
{
for (QName disabledBehaviour: disabledBehaviours)
{
behaviourFilter.disableBehaviour(importedRef.context.getNodeRef(), disabledBehaviour);
}
nodeService.setProperty(importedRef.context.getNodeRef(), importedRef.property, refProperty);
if (progress != null)
{
progress.propertySet(importedRef.context.getNodeRef(), importedRef.property, refProperty);
}
}
finally
{
behaviourFilter.enableBehaviours(importedRef.context.getNodeRef());
}
// [talendalfresco] ending transaction
userTransaction.commit();
long end = System.nanoTime();
// success handling
// TODO log mode
contentImporterBinding.contentImporterResultHandler.referenceSuccess(
importedRef.context.getNodeRef(), importedRef.property, importedRef.value, "in " + (end - start) + "ns");
} catch (Throwable t) {
try {
if (userTransaction != null) {
userTransaction.rollback();
}
} catch (Exception ex) {
logger.debug("Unknown exception while rollbacking", ex);
}
// error handling
contentImporterBinding.contentImporterResultHandler.referenceError(
importedRef.context.getNodeRef(), importedRef.property, importedRef.value, t);
}
}
reportCompleted();
}
}
public void setTransactionService(TransactionService transactionService) {
this.transactionService = transactionService;
}
public void setMimetypeService(MimetypeService mimetypeService) {
this.mimetypeService = mimetypeService;
}
public void setNamePathService(NamePathService namePathService) {
this.namePathService = namePathService;
}
}