Index: ivy.xml =================================================================== --- ivy.xml (revision 19862) +++ ivy.xml (working copy) @@ -49,6 +49,7 @@ + Index: main/plugin-cms/plugin.xml =================================================================== --- main/plugin-cms/plugin.xml (revision 19862) +++ main/plugin-cms/plugin.xml (working copy) @@ -4966,4 +4966,15 @@ + + + + + + + + Index: main/plugin-cms/src/org/ametys/cms/io/IOUtils.java =================================================================== --- main/plugin-cms/src/org/ametys/cms/io/IOUtils.java (revision 0) +++ main/plugin-cms/src/org/ametys/cms/io/IOUtils.java (revision 0) @@ -0,0 +1,205 @@ +/* + * Copyright 2012 Anyware Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ametys.cms.io; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.query.Query; +import javax.jcr.query.QueryManager; +import javax.jcr.version.Version; +import javax.jcr.version.VersionHistory; +import javax.jcr.version.VersionIterator; + +import org.apache.cocoon.util.HashUtil; +import org.apache.commons.lang.ArrayUtils; +import org.apache.commons.lang.StringUtils; + +import org.ametys.plugins.workflow.store.JackrabbitWorkflowStore; + +/** + * Collection of static utility methods to be used in the import/export + */ +public final class IOUtils +{ + /** + * Private constructor to prevent instantiation of this class. + */ + private IOUtils() + { + } + + /** + * Copy of the {@link JackrabbitWorkflowStore#_getEntryHash} method. + * @param name The name to be hashed + * @return a list containing the hash. + */ + public static List getHashedName(String name) + { + long hash = Math.abs(HashUtil.hash(name)); + String hashHexa = StringUtils.leftPad(Long.toString(hash, 16), 4, "0"); + + List hashSegments = new ArrayList(); + hashSegments.add(hashHexa.substring(0, 2)); + hashSegments.add(hashHexa.substring(2, 4)); + return hashSegments; + } + + /** + * Retrieves versionable nodes that are descendant of the node passed as argument. + * @param node the reference node. + * @return a {@link NodeIterator} over all nodes that match that are versionable + * @throws RepositoryException + */ + public static NodeIterator retrieveVersionableNodes(Node node) throws RepositoryException + { + QueryManager queryManager = node.getSession().getWorkspace().getQueryManager(); + + // Xpath query replacing hash node by * to avoid getting an error with node name starting with a number. + String xpathExp = "/jcr:root" + node.getPath().replaceAll("/[0-9][^/]", "/*"); + xpathExp += "//element(*, mix:versionable)"; + + @SuppressWarnings("deprecation") + Query query = queryManager.createQuery(xpathExp, Query.XPATH); + + return query.execute().getNodes(); + } + + /** + * Returns an iterator over all versions of a version history order by date of creation (descending) + * @param versionHistory + * @return an {@link Iterator} on {@link Version} + * @throws RepositoryException + */ + public static Iterator getVersionSortedByCreatedDesc(VersionHistory versionHistory) throws RepositoryException + { + VersionIterator versionIterator = versionHistory.getAllVersions(); + + // Sort by date descendant + List versions = new ArrayList(); + while (versionIterator.hasNext()) + { + versions.add(versionIterator.nextVersion()); + } + + Collections.sort(versions, new Comparator() + { + public int compare(Version v1, Version v2) + { + try + { + return v2.getCreated().getTime().compareTo(v1.getCreated().getTime()); + } + catch (RepositoryException e) + { + throw new RuntimeException("Unable to retrieve a creation date", e); + } + } + }); + + return versions.iterator(); + } + + /** + * Encode the string as a safe zip entry name. Every UNC (see Universal + * Naming Convention) reserved characters are encoded except for the '/' + * (forward slash) which denotes a directory in zip entry name. + * @param str The name to be encoded. + * @return The encoded string + */ + public static String encodeZipEntryName(String str) + { + boolean needToChange = false; + final char escape = '%'; + final char[] needEncoding = {'\\', ':', '*', '?', '"', '<', '>', '|', '+'}; + int len = str.length(); + + StringBuilder sb = new StringBuilder(len); + for (int i = 0; i < len; i++) + { + char ch = str.charAt(i); + if (ArrayUtils.contains(needEncoding, ch)) + { + sb.append(escape); + sb.append(Integer.toHexString(ch).toUpperCase()); + needToChange = true; + } + else + { + sb.append(ch); + } + } + + return needToChange ? sb.toString() : str; + } + + /** + * Inverse method of the encoding function above. + * Decode a previously encoded string such as :
+ * str = decode(encode(str)); + * @param str The name to be decoded + * @return The decoded string + */ + public static String decodeZipEntryName(String str) + { + boolean needToChange = false; + final char escape = '%'; + final char[] allowedDecoding = {'\\', ':', '*', '?', '"', '<', '>', '|', '+'}; + int len = str.length(); + + StringBuilder sb = new StringBuilder(len); + int i = -1; + while (++i < len) + { + char ch = str.charAt(i); + if (ch == escape) + { + try + { + String hexCode = str.substring(i + 1, i + 3); + + char decoded = (char) Integer.parseInt(hexCode, 16); + if (ArrayUtils.contains(allowedDecoding, decoded)) + { + sb.append(decoded); + i += 2; + needToChange = true; + } + else + { + sb.append(ch); + } + } + catch (IndexOutOfBoundsException e) + { + // Ignores, do not decode + sb.append(ch); + } + } + else + { + sb.append(ch); + } + } + return needToChange ? sb.toString() : str; + } +} Index: main/plugin-cms/src/org/ametys/cms/io/exporters/BinaryPropertiesExporter.java =================================================================== --- main/plugin-cms/src/org/ametys/cms/io/exporters/BinaryPropertiesExporter.java (revision 0) +++ main/plugin-cms/src/org/ametys/cms/io/exporters/BinaryPropertiesExporter.java (revision 0) @@ -0,0 +1,96 @@ +/* + * Copyright 2012 Anyware Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ametys.cms.io.exporters; + +import java.io.OutputStream; +import java.util.Collection; +import java.util.Map; +import java.util.Map.Entry; + +import javax.jcr.Node; +import javax.jcr.Property; + +import org.apache.avalon.framework.logger.Logger; + +import org.ametys.runtime.util.LoggerFactory; + +/** + * JcrExporter for binary properties. + */ +public class BinaryPropertiesExporter extends JcrExporter +{ + /** logger */ + protected final Logger _logger = LoggerFactory.getLoggerFor(BinaryPropertiesExporter.class); + + /** Base node. Path manipulated in this class are must be relative to this node. */ + protected final Node _node; + /** Map containing information about the collected binary properties */ + protected final Map> _binaryMap; + /** The export outputStream */ + protected final OutputStream _outputStream; + + /** + * Ctor + * @param node {@link #_node} + * @param binaryMap {@link #_binaryMap} + * @param outputStream {@link #_outputStream} + */ + public BinaryPropertiesExporter(Node node, Map> binaryMap, OutputStream outputStream) + { + _node = node; + _binaryMap = binaryMap; + _outputStream = outputStream; + } + + /** + * Export method. This is where the job is actually done. + * @throws Exception + */ + public void export() throws Exception + { + // Copy of the binary data + for (Entry> binaryMapEntry : _binaryMap.entrySet()) + { + exportByNode(binaryMapEntry.getKey(), binaryMapEntry.getValue()); + } + } + + private void exportByNode(String relPath, Collection propertyNames) throws Exception + { + Node node = _node.getNode(relPath); + + // Export each of the binary properties for this node + for (String propertyName : propertyNames) + { + Property property = node.getProperty(propertyName); + exportProperty(property); + } + } + + private void exportProperty(Property property) throws Exception + { + try + { + exportBinaryProperty(property, _outputStream); + } + catch (Exception e) + { + // Log and re-throw + _logger.error("Unable to export binary property with path '" + property.getPath() + "'", e); + throw e; + } + } +} Index: main/plugin-cms/src/org/ametys/cms/io/exporters/ResourcesExporter.java =================================================================== --- main/plugin-cms/src/org/ametys/cms/io/exporters/ResourcesExporter.java (revision 0) +++ main/plugin-cms/src/org/ametys/cms/io/exporters/ResourcesExporter.java (revision 0) @@ -0,0 +1,121 @@ +/* + * Copyright 2012 Anyware Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ametys.cms.io.exporters; + +import java.io.OutputStream; +import java.util.Set; + +import javax.jcr.Node; +import javax.jcr.Property; + +import org.apache.avalon.framework.logger.Logger; +import org.apache.jackrabbit.JcrConstants; +import org.xml.sax.ContentHandler; + +import org.ametys.cms.io.handlers.ExportResourcesHandler; +import org.ametys.runtime.util.LoggerFactory; + +/** + * JcrExporter for ametys resources. + */ +public class ResourcesExporter extends JcrExporter +{ + /** Relative path from a resource node to its data property */ + public static final String JCR_DATA_PATH = JcrConstants.JCR_CONTENT + '/' + JcrConstants.JCR_DATA; + + /** logger */ + protected final Logger _logger = LoggerFactory.getLoggerFor(ResourcesExporter.class); + + /** Base node. Path manipulated in this class are must be relative to this node. */ + protected final Node _node; + /** Set containing the path to the collected ametys resources to be exported */ + protected final Set _resourcePaths; + /** The export handler to which some node are serialized (using the XML system view JCR export) */ + protected final ContentHandler _contentHandler; + /** The export outputStream in which binary resource will be copied */ + protected final OutputStream _outputStream; + + /** + * Ctor + * @param node + * @param resourcePaths + * @param contentHandler + * @param outputStream + */ + public ResourcesExporter(Node node, Set resourcePaths, ContentHandler contentHandler, OutputStream outputStream) + { + _node = node; + _resourcePaths = resourcePaths; + _contentHandler = contentHandler; + _outputStream = outputStream; + } + + /** + * Export method. This is where the job is actually done. + * @throws Exception + */ + public void export() throws Exception + { + for (String relPath : _resourcePaths) + { + exportResourceNode(relPath); + } + } + + private void exportResourceNode(String relPath) throws Exception + { + try + { + final Node resourceNode = _node.getNode(relPath); + ExportResourcesHandler exportResourcesHandler = new ExportResourcesHandler(_contentHandler, resourceNode); + exportNode(resourceNode, exportResourcesHandler, true, false); // skip binary, they will be exported later. + + exportResourceHierarchy(resourceNode, exportResourcesHandler.getResources()); + } + catch (Exception e) + { + // Log and re-throw + _logger.error("Unable to export resource collection node with path '" + _node.getPath() + '/' + relPath + "'", e); + throw e; + } + } + + private void exportResourceHierarchy(Node resourceNode, Set resourceRelPaths) throws Exception + { + for (String relPath : resourceRelPaths) + { + if (!relPath.endsWith("/")) + { + Property property = resourceNode.getNode(relPath).getProperty(JCR_DATA_PATH); + exportProperty(property); + } + } + } + + private void exportProperty(Property property) throws Exception + { + try + { + exportBinaryProperty(property, _outputStream); + } + catch (Exception e) + { + // Log and re-throw + _logger.error("Unable to export resource property with path '" + property.getPath() + "'", e); + throw e; + } + } +} Index: main/plugin-cms/src/org/ametys/cms/io/exporters/LogExporter.java =================================================================== --- main/plugin-cms/src/org/ametys/cms/io/exporters/LogExporter.java (revision 0) +++ main/plugin-cms/src/org/ametys/cms/io/exporters/LogExporter.java (revision 0) @@ -0,0 +1,112 @@ +/* + * Copyright 2012 Anyware Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ametys.cms.io.exporters; + +import java.io.OutputStream; +import java.io.PrintStream; +import java.util.Collection; +import java.util.Map; +import java.util.Properties; + +import javax.jcr.Node; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.avalon.framework.logger.Logger; + +import org.ametys.runtime.util.LoggerFactory; + +/** + * LogExporter + *

+ * Currently it logs : + *

    + *
  • references that points to an Item (jcr) which is located + * outside of the scope of the export
  • + *
+ */ +public class LogExporter +{ + /** Log file filename */ + public static final String LOG_FILE_NAME = "log.txt"; + + /** External references property node name */ + public static final String EXTERNAL_REF_PROPERTY_NODE_NAME = "nodeName"; + /** External references property node path */ + public static final String EXTERNAL_REF_PROPERTY_NODE_PATH = "nodePath"; + /** External references property reference type */ + public static final String EXTERNAL_REF_PROPERTY_NAME = "referenceName"; + /** External references property reference type */ + public static final String EXTERNAL_REF_PROPERTY_TYPE = "referenceType"; + /** External references property reference value */ + public static final String EXTERNAL_REF_PROPERTY_VALUE = "referenceValue"; + + /** logger */ + protected final Logger _logger = LoggerFactory.getLoggerFor(LogExporter.class); + + /** Map containing collected external references to be logged. */ + protected final Map> _externalReferences; + /** Session in which the export is done */ + protected final Session _session; + /** The export outputStream */ + protected final OutputStream _outputStream; + + + /** + * Ctor + * @param externalReferences {@link #_externalReferences} + * @param session {@link #_session} + * @param outputStream {@link LogExporter#_outputStream} + */ + public LogExporter(Map> externalReferences, Session session, OutputStream outputStream) + { + _externalReferences = externalReferences; + _session = session; + _outputStream = outputStream; + } + + /** + * Export method. This is where the job is actually done. + * @throws RepositoryException + */ + public void export() throws RepositoryException + { + PrintStream ps = new PrintStream(_outputStream); + + final String format = "External references found for node '%s'%nIdentifier '%s' | Path '%s'%n"; + for (String nodeIdentifier : _externalReferences.keySet()) + { + Node node = _session.getNodeByIdentifier(nodeIdentifier); + ps.printf(format, node.getName(), nodeIdentifier, node.getPath()); + _exportLog(_externalReferences.get(nodeIdentifier), ps); + ps.println(); + } + } + + private void _exportLog(Collection propertiesList, PrintStream ps) + { + final String format = "\t[%s] Property '%s' { type '%s' | value '%s'}%n"; + for (Properties properties : propertiesList) + { + String refName = (String) properties.get(EXTERNAL_REF_PROPERTY_NAME); + String refType = (String) properties.get(EXTERNAL_REF_PROPERTY_TYPE); + String refValue = (String) properties.get(EXTERNAL_REF_PROPERTY_VALUE); + String logLevel = PropertyType.TYPENAME_REFERENCE.equals(refType) ? "Warn" : "Info"; + ps.printf(format, logLevel, refName, refType, refValue); + } + } +} Index: main/plugin-cms/src/org/ametys/cms/io/exporters/DefaultJcrExportListener.java =================================================================== --- main/plugin-cms/src/org/ametys/cms/io/exporters/DefaultJcrExportListener.java (revision 0) +++ main/plugin-cms/src/org/ametys/cms/io/exporters/DefaultJcrExportListener.java (revision 0) @@ -0,0 +1,51 @@ +/* + * Copyright 2012 Anyware Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ametys.cms.io.exporters; + +import javax.jcr.Node; +import javax.jcr.Property; + + +/** + * DefaultJcrExportListener that does actually nothing. + * JcrExportListener may extends this class to avoid defining each method of the interface. + */ +public class DefaultJcrExportListener implements JcrExportListener +{ + @Override + public void onBeforeNodeExport(Node node) throws Exception + { + // nothing + } + + @Override + public void onAfterNodeExport(Node node) throws Exception + { + // nothing + } + + @Override + public void onBeforeBinaryPropertyExport(Property property) throws Exception + { + // nothing + } + + @Override + public void onAfterBinaryPropertyExport(Property property) throws Exception + { + // nothing + } +} Index: main/plugin-cms/src/org/ametys/cms/io/exporters/WorkflowExporter.java =================================================================== --- main/plugin-cms/src/org/ametys/cms/io/exporters/WorkflowExporter.java (revision 0) +++ main/plugin-cms/src/org/ametys/cms/io/exporters/WorkflowExporter.java (revision 0) @@ -0,0 +1,82 @@ +/* + * Copyright 2012 Anyware Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ametys.cms.io.exporters; + +import java.util.Set; + +import javax.jcr.Node; +import javax.jcr.Session; + +import org.apache.avalon.framework.logger.Logger; +import org.xml.sax.ContentHandler; + +import org.ametys.runtime.util.LoggerFactory; + +/** + * JcrExporter for workflow nodes. + */ +public class WorkflowExporter extends JcrExporter +{ + /** logger */ + protected final Logger _logger = LoggerFactory.getLoggerFor(WorkflowExporter.class); + + /** Set containing the reference to the workflow entry nodes to export */ + protected final Set _workflowRefs; + /** Session in which the export is done */ + protected final Session _session; + /** The export handler */ + protected final ContentHandler _contentHandler; + + /** + * Ctor + * @param workflowRefs + * @param session + * @param contentHandler + */ + public WorkflowExporter(Set workflowRefs, Session session, ContentHandler contentHandler) + { + _workflowRefs = workflowRefs; + _session = session; + _contentHandler = contentHandler; + } + + /** + * Export method. This is where the job is actually done. + * @throws Exception + */ + public void export() throws Exception + { + for (String identifier : _workflowRefs) + { + exportWorkflow(identifier); + } + } + + private void exportWorkflow(String identifier) throws Exception + { + try + { + final Node node = _session.getNodeByIdentifier(identifier); + exportNode(node, _contentHandler, false, false); + } + catch (Exception e) + { + // Log and re-throw + _logger.error("Unable to export workflow node with identifier '" + identifier + "'", e); + throw e; + } + } +} Index: main/plugin-cms/src/org/ametys/cms/io/exporters/JcrExporter.java =================================================================== --- main/plugin-cms/src/org/ametys/cms/io/exporters/JcrExporter.java (revision 0) +++ main/plugin-cms/src/org/ametys/cms/io/exporters/JcrExporter.java (revision 0) @@ -0,0 +1,172 @@ +/* + * Copyright 2012 Anyware Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ametys.cms.io.exporters; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.GregorianCalendar; + +import javax.jcr.Binary; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.commons.io.IOUtils; +import org.xml.sax.ContentHandler; + +import org.ametys.plugins.repository.RepositoryConstants; + +/** + * Instance of this class can be used to export JCR data (nodes, properties). It + * is possible to bind a listener that will listen to onBefore and onAfter + * export events. + */ +public class JcrExporter +{ + /** Ametys internal export date property */ + public static final String EXPORT_DATE_PROPERTY = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":exportDate"; + + /** listener */ + protected JcrExportListener _listener; + + + /** + * JCR export of a Node into a contentHandler. + * Using JCR System View. + * @param node The node to export + * @param contentHandler the contentHandler to which the SAX events representing the XML serialization of the node will be output. + * @param skipBinary true to skip binary property during the export. + * @param setExportDate true to dynamically set an export data as a property of the exported node (it will only appears in the exported XML, the session used during the export will not be saved). + * @throws Exception + */ + public void exportNode(Node node, ContentHandler contentHandler, boolean skipBinary, boolean setExportDate) throws Exception + { + final Session session = node.getSession(); + final String path = node.getPath(); + + if (setExportDate) + { + setExportDate(node); + } + + onBeforeNodeExport(node); + session.exportSystemView(path, contentHandler, skipBinary, false); + onAfterNodeExport(node); + } + + /** + * JCR export of a Node into a contentHandler. + * Using JCR System View. + * @param node The node to export + * @param outputStream the outputStream to which the XML serialization of the node will be output. + * @param skipBinary true to skip binary property during the export. + * @param setExportDate true to dynamically set an export data as a property of the exported node (it will only appears in the exported XML, the session used during the export will not be saved). + * @throws Exception + */ + public void exportNode(Node node, OutputStream outputStream, boolean skipBinary, boolean setExportDate) throws Exception + { + final Session session = node.getSession(); + final String path = node.getPath(); + + if (setExportDate) + { + setExportDate(node); + } + + onBeforeNodeExport(node); + session.exportSystemView(path, outputStream, skipBinary, false); + onAfterNodeExport(node); + } + + /** + * Convenient method to export (copy) a JCR binary property into a outputStream. + * @param property The property to export + * @param outputStream The outputStream to which the binary data will be output. + * @throws Exception + */ + public void exportBinaryProperty(Property property, OutputStream outputStream) throws Exception + { + Binary binary = property.getBinary(); + InputStream is = null; + try + { + is = binary.getStream(); + + onBeforeBinaryPropertyExport(property); + IOUtils.copy(is, outputStream); + onAfterBinaryPropertyExport(property); + } + finally + { + IOUtils.closeQuietly(is); + binary.dispose(); + } + } + + /** + * Setter for the listener + * @param listener + */ + public void setListener(JcrExportListener listener) + { + _listener = listener; + } + + /** + * Set the export date to a node. + * @param node + * @throws RepositoryException + */ + private void setExportDate(Node node) throws RepositoryException + { + final GregorianCalendar gc = new GregorianCalendar(); + node.setProperty(EXPORT_DATE_PROPERTY, gc); + } + + // ------------- listener methods ----------------------------------------< + private void onBeforeNodeExport(Node node) throws Exception + { + if (_listener != null) + { + _listener.onBeforeNodeExport(node); + } + } + + private void onAfterNodeExport(Node node) throws Exception + { + if (_listener != null) + { + _listener.onAfterNodeExport(node); + } + } + + private void onBeforeBinaryPropertyExport(Property property) throws Exception + { + if (_listener != null) + { + _listener.onBeforeBinaryPropertyExport(property); + } + } + + private void onAfterBinaryPropertyExport(Property property) throws Exception + { + if (_listener != null) + { + _listener.onAfterBinaryPropertyExport(property); + } + } +} Index: main/plugin-cms/src/org/ametys/cms/io/exporters/JcrExportListener.java =================================================================== --- main/plugin-cms/src/org/ametys/cms/io/exporters/JcrExportListener.java (revision 0) +++ main/plugin-cms/src/org/ametys/cms/io/exporters/JcrExportListener.java (revision 0) @@ -0,0 +1,51 @@ +/* + * Copyright 2012 Anyware Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ametys.cms.io.exporters; + +import javax.jcr.Node; +import javax.jcr.Property; + +/** + * Listener used during an export made with the {@link JcrExporter} + */ +public interface JcrExportListener +{ + /** + * Listener called just before the export of a node. + * @param node + * @throws Exception + */ + public void onBeforeNodeExport(Node node) throws Exception; + /** + * Listener called just after the export of a node. + * @param node + * @throws Exception + */ + public void onAfterNodeExport(Node node) throws Exception; + + /** + * Listener called just before the export of a binary property + * @param property + * @throws Exception + */ + public void onBeforeBinaryPropertyExport(Property property) throws Exception; + /** + * Listener called just before the export of a binary property + * @param property + * @throws Exception + */ + public void onAfterBinaryPropertyExport(Property property) throws Exception; +} Index: main/plugin-cms/src/org/ametys/cms/io/exporters/AmetysExporter.java =================================================================== --- main/plugin-cms/src/org/ametys/cms/io/exporters/AmetysExporter.java (revision 0) +++ main/plugin-cms/src/org/ametys/cms/io/exporters/AmetysExporter.java (revision 0) @@ -0,0 +1,150 @@ +/* + * Copyright 2012 Anyware Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ametys.cms.io.exporters; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import org.apache.avalon.framework.logger.Logger; +import org.xml.sax.ContentHandler; + +import org.ametys.runtime.util.LoggerFactory; + +/** + * Abstract Ametys Exporter. + * Ametys Exporters (as SiteExporter, ODFExporter etc...) should extend this class. + */ +public abstract class AmetysExporter +{ + // Existing configurable properties + /** content handler property */ + public static final String CONTENT_HANDLER_PROPERTY = "contentHandler"; + /** output stream property */ + public static final String OUTPUT_STREAM_PROPERTY = "outputStream"; + /** listener property */ + public static final String LISTENER_PROPERTY = "listener"; + + /** Map binding configurable properties to their expected class. */ + protected Map _propertyTypes = new HashMap(); + + /** logger */ + protected final Logger _logger = LoggerFactory.getLoggerFor(AmetysExporter.class); + + /** Map containing the properties for each exporter used during the global export */ + protected final Map _exportProperties = new HashMap(); + + /** + * Run the export + * @throws IOException if an error occurs + */ + public void export() throws IOException + { + initialize(); + _exportInternal(); + } + + /** + * Set the properties for an exporter + * @param key The key of the exporter + * @param properties The configuration of the exporter + */ + public void setProperties(String key, Properties properties) + { + _exportProperties.put(key, properties); + } + + /** + * Initialize the ametys exporter + */ + protected void initialize() + { + _propertyTypes.put(CONTENT_HANDLER_PROPERTY, ContentHandler.class); + _propertyTypes.put(OUTPUT_STREAM_PROPERTY, OutputStream.class); + _propertyTypes.put(LISTENER_PROPERTY, JcrExportListener.class); + } + + /** + * Internal export method where the job should be done. + * @throws IOException + */ + protected abstract void _exportInternal() throws IOException; + + /** + * Helper method retrieving the properties of an exporter used during the global export. + * It also check the integrity of these properties. + * @param key of the exporter + * @param mandatoryProps List of properties that are mandatory + * @param optionalProps List of properties that are optional + * @return The retrieved properties + */ + protected Properties _internalCheckAndGetProperties(String key, List mandatoryProps, List optionalProps) + { + Properties props = _exportProperties.get(key); + if (props == null) + { + final String msg = "Unable to proceed to the export. '" + key + "' properties are not set."; + _logger.error(msg); + throw new IllegalStateException(msg); + } + + // Check mandatory properties + for (String propKey : mandatoryProps) + { + Object o = props.get(propKey); + if (o != null) + { + if (!_internalTestPropertyInstance(propKey, o)) + { + final String msg = "Unable to proceed to the export. Mandatory property '" + propKey + "' has not the expected type (properties '" + key + "')."; + _logger.error(msg); + throw new IllegalStateException(msg); + } + } + else + { + final String msg = "Unable to proceed to the export. Mandatory property '" + propKey + "' is not set for properties '" + key + "'."; + _logger.error(msg); + throw new IllegalStateException(msg); + } + } + + // Check optional properties + for (String propKey : optionalProps) + { + Object o = props.get(propKey); + if (o != null) + { + if (!_internalTestPropertyInstance(propKey, o)) + { + final String msg = "Unable to proceed to the export. Optional property '" + propKey + "' has not the expected type (properties '" + key + "')."; + _logger.error(msg); + throw new IllegalStateException(msg); + } + } + } + + return props; + } + + private boolean _internalTestPropertyInstance(String propKey, Object o) + { + return _propertyTypes.containsKey(propKey) && _propertyTypes.get(propKey).isInstance(o); + } +} Index: main/plugin-cms/src/org/ametys/cms/io/importers/JcrImporter.java =================================================================== --- main/plugin-cms/src/org/ametys/cms/io/importers/JcrImporter.java (revision 0) +++ main/plugin-cms/src/org/ametys/cms/io/importers/JcrImporter.java (revision 0) @@ -0,0 +1,130 @@ +/* + * Copyright 2012 Anyware Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ametys.cms.io.importers; + +import java.io.IOException; +import java.io.InputStream; + +import javax.jcr.Binary; +import javax.jcr.ImportUUIDBehavior; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.ValueFactory; + +import org.apache.avalon.framework.component.Component; +import org.apache.avalon.framework.logger.AbstractLogEnabled; +import org.apache.avalon.framework.service.ServiceException; +import org.apache.avalon.framework.service.ServiceManager; +import org.apache.avalon.framework.service.Serviceable; +import org.apache.commons.io.IOUtils; +import org.apache.excalibur.xml.sax.ContentHandlerProxy; +import org.apache.excalibur.xml.sax.SAXParser; +import org.xml.sax.ContentHandler; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +/** + * JcrImporter + */ +public class JcrImporter extends AbstractLogEnabled implements Component, Serviceable +{ + /** SAX Parser */ + protected SAXParser _saxParser; + + @Override + public void service(ServiceManager manager) throws ServiceException + { + _saxParser = (SAXParser) manager.lookup(SAXParser.ROLE); + } + + /** + * JCR import of a Node into, using JCR System View. The incoming + * inputStream will not be closed during the execution of this method. + *

+ * The passed inputStream is closed before this method returns either + * normally or because of an exception. + * + * @param parentNode Node in which the import will be done. + * @param uuidBehavior the {@link ImportUUIDBehavior} to be used during the + * import. + * @param inputStream Input JCR System view XML to be imported. + * @param importHandlerProxy An optional import handler proxy that will act + * as a bridge between the inputStream and the JCR import + * handler. + * @throws RepositoryException + * @throws IOException + * @throws SAXException + */ + public void importNode(Node parentNode, int uuidBehavior, InputStream inputStream, ContentHandlerProxy importHandlerProxy) throws RepositoryException, SAXException, IOException + { + try + { + final Session session = parentNode.getSession(); + final String parentAbsPath = parentNode.getPath(); + + // JCR import handler + ContentHandler importHandler = session.getImportContentHandler(parentAbsPath, uuidBehavior); + + // Optional proxy handler + if (importHandlerProxy != null) + { + importHandlerProxy.setContentHandler(importHandler); + importHandler = importHandlerProxy; + } + + // Parse the input source and send the events to the import handler + InputSource inputSource = new InputSource(inputStream); + _saxParser.parse(inputSource, importHandler); + } + finally + { + IOUtils.closeQuietly(inputStream); + } + } + + /** + * Convenient method to import an inputStream as a JCR binary property of a + * Node. + *

+ * The passed inputStream is closed before this method returns either + * normally or because of an exception. + * + * @param node Node for which the property will be set. + * @param propertyName The name of the property to set. + * @param inputStream The inputStream consuming the binary data to import. + * @throws RepositoryException + */ + public void importBinaryProperty(Node node, String propertyName, InputStream inputStream) throws RepositoryException + { + Binary binary = null; + try + { + final ValueFactory vf = node.getSession().getValueFactory(); + binary = vf.createBinary(inputStream); + node.setProperty(propertyName, binary); + } + finally + { + IOUtils.closeQuietly(inputStream); + + if (binary != null) + { + binary.dispose(); + } + } + } +} Index: main/plugin-cms/src/org/ametys/cms/io/importers/BinaryPropertiesImporter.java =================================================================== --- main/plugin-cms/src/org/ametys/cms/io/importers/BinaryPropertiesImporter.java (revision 0) +++ main/plugin-cms/src/org/ametys/cms/io/importers/BinaryPropertiesImporter.java (revision 0) @@ -0,0 +1,79 @@ +/* + * Copyright 2012 Anyware Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ametys.cms.io.importers; + +import java.io.InputStream; +import java.util.List; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +/** + * Binary properties importer using the JCR Import. + */ +public class BinaryPropertiesImporter extends JcrImporter +{ + /** Avalon role. */ + public static final String ROLE = BinaryPropertiesImporter.class.getName(); + + /** input data list property */ + public static final String IMPORT_DATA_LIST_PROPERTY = "importDataList"; + + /** + * Import binary properties + * @param importSession + * @param importDataList + * @throws RepositoryException + */ + public void doImport(Session importSession, List importDataList) throws RepositoryException + { + for (ImportData data : importDataList) + { + Node node = importSession.getNodeByIdentifier(data._nodeIdentifier); + importBinaryProperty(node, data._propertyName, data._binaryStream); + + importSession.save(); + } + } + + /** + * Import data structure used to execute the import. An instance of this + * class corresponds to an binary property to import. + */ + public static class ImportData + { + /** The JCR identifier of the node on which the property will be set */ + protected String _nodeIdentifier; + /** The name of the property to import */ + protected String _propertyName; + /** Inputstream consuming the binary data to import */ + protected InputStream _binaryStream; + + /** + * Ctor + * @param nodeIdentifier + * @param propertyName + * @param binaryStream + */ + public ImportData(String nodeIdentifier, String propertyName, InputStream binaryStream) + { + _nodeIdentifier = nodeIdentifier; + _propertyName = propertyName; + _binaryStream = binaryStream; + } + } +} Index: main/plugin-cms/src/org/ametys/cms/io/importers/WorkflowImporter.java =================================================================== --- main/plugin-cms/src/org/ametys/cms/io/importers/WorkflowImporter.java (revision 0) +++ main/plugin-cms/src/org/ametys/cms/io/importers/WorkflowImporter.java (revision 0) @@ -0,0 +1,82 @@ +/* + * Copyright 2012 Anyware Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ametys.cms.io.importers; + +import java.io.IOException; +import java.io.InputStream; + +import javax.jcr.ImportUUIDBehavior; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.commons.JcrUtils; +import org.xml.sax.SAXException; + +import org.ametys.cms.io.IOConstants; +import org.ametys.cms.io.handlers.ImportWorkflowHandler; + +/** + * Workflow importer using the JCR Import. + */ +public class WorkflowImporter extends JcrImporter +{ + /** Avalon role. */ + public static final String ROLE = WorkflowImporter.class.getName(); + + /** Workflow input stream property */ + public static final String INPUT_STREAM_PROPERTY = "inputStream"; + + /** + * Import workflow nodes from an inputstream. + * @param importSession The session in which the import will be done. + * @param defaultSession + * @param inputStream + * @param referenceTracker + * @return the importWorkflowHandler + * @throws RepositoryException + * @throws SAXException + * @throws IOException + */ + public ImportWorkflowHandler doImport(Session importSession, Session defaultSession, InputStream inputStream, ImportReferenceTracker referenceTracker) throws RepositoryException, SAXException, IOException + { + Node rootWfNodeDefaultWS = defaultSession.getRootNode().getNode(IOConstants.OSWF_ROOT); + Node rootWorkflowNode = _getRootWorkflowNode(importSession, defaultSession, rootWfNodeDefaultWS); + + // Get the import handler + int uuidBehavior = ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW; // We can create new uuid for workflow entry, to reduce collision risk. + long nextEntryId = rootWfNodeDefaultWS.getProperty(IOConstants.NEXT_ENTRY_ID_PROPERTY).getLong(); + ImportWorkflowHandler importWorkflowHandler = new ImportWorkflowHandler(rootWorkflowNode, uuidBehavior, referenceTracker, nextEntryId); + + // Performing the import. + importNode(rootWorkflowNode, uuidBehavior, inputStream, importWorkflowHandler); + + // Update root workflow node (default workspace) next entry id property + rootWfNodeDefaultWS.setProperty(IOConstants.NEXT_ENTRY_ID_PROPERTY, importWorkflowHandler.getCurrentEntryId() + 1); + + return importWorkflowHandler; + } + + private Node _getRootWorkflowNode(Session importSession, Session defaultSession, Node rootWfNodeDefaultWS) throws RepositoryException + { + if (!importSession.getWorkspace().getName().equals(defaultSession.getWorkspace().getName())) + { + return JcrUtils.getOrAddNode(importSession.getRootNode(), IOConstants.OSWF_ROOT, IOConstants.OSWF_ROOT_NT); + } + + return rootWfNodeDefaultWS; + } +} Index: main/plugin-cms/src/org/ametys/cms/io/importers/jcr/versions/VersionImporterModifier.java =================================================================== --- main/plugin-cms/src/org/ametys/cms/io/importers/jcr/versions/VersionImporterModifier.java (revision 0) +++ main/plugin-cms/src/org/ametys/cms/io/importers/jcr/versions/VersionImporterModifier.java (revision 0) @@ -0,0 +1,92 @@ +/* + * Copyright 2012 Anyware Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ametys.cms.io.importers.jcr.versions; + +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.ProtectedItemModifier; +import org.apache.jackrabbit.core.security.authorization.Permission; +import org.apache.jackrabbit.spi.commons.name.NameConstants; + +/** + * VersionImporterModifier allows to modify version properties by + * extending ProtectedItemModifier + */ +public class VersionImporterModifier extends ProtectedItemModifier +{ + /** + * VersionImporterModifier ctor + */ + public VersionImporterModifier() + { + super(Permission.VERSION_MNGMT); + } + + /** + * Set the internal value of a the {@link NameConstants#JCR_VERSIONHISTORY} + * property without checking any constraints. + * @param parentImpl + * @param value + * @return the modified property + * @throws RepositoryException + */ + public Property setVersionHistoryProperty(NodeImpl parentImpl, Value value) throws RepositoryException + { + return setProperty(parentImpl, NameConstants.JCR_VERSIONHISTORY, value); + } + + /** + * Set the internal value of a the {@link NameConstants#JCR_BASEVERSION} + * property without checking any constraints. + * @param parentImpl + * @param value + * @return the modified property + * @throws RepositoryException + */ + public Property setBaseVersionProperty(NodeImpl parentImpl, Value value) throws RepositoryException + { + return setProperty(parentImpl, NameConstants.JCR_BASEVERSION, value); + } + + /** + * Set the internal value of a the {@link NameConstants#JCR_VERSIONHISTORY} + * property without checking any constraints. + * @param parentImpl + * @param values + * @return the modified property + * @throws RepositoryException + */ + public Property setPredecessorsProperty(NodeImpl parentImpl, Value[] values) throws RepositoryException + { + return setProperty(parentImpl, NameConstants.JCR_PREDECESSORS, values); + } + + /** + * Set the internal value of a the {@link NameConstants#JCR_ISCHECKEDOUT} + * property without checking any constraints. + * @param parentImpl + * @param value + * @return the modified property + * @throws RepositoryException + */ + public Property setIsCheckedOutProperty(NodeImpl parentImpl, Value value) throws RepositoryException + { + return setProperty(parentImpl, NameConstants.JCR_ISCHECKEDOUT, value); + } +} Index: main/plugin-cms/src/org/ametys/cms/io/importers/jcr/versions/VersionImporter.java =================================================================== --- main/plugin-cms/src/org/ametys/cms/io/importers/jcr/versions/VersionImporter.java (revision 0) +++ main/plugin-cms/src/org/ametys/cms/io/importers/jcr/versions/VersionImporter.java (revision 0) @@ -0,0 +1,147 @@ +/* + * Copyright 2012 Anyware Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ametys.cms.io.importers.jcr.versions; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +import org.apache.jackrabbit.api.JackrabbitSession; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.util.ReferenceChangeTracker; +import org.apache.jackrabbit.core.xml.DefaultProtectedPropertyImporter; +import org.apache.jackrabbit.core.xml.PropInfo; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; + +/** + * VersionImporter implements a DefaultProtectedPropertyImporter + * that set the following internal version properties which are protected by default : + *

    + *
  • {@link NameConstants#JCR_VERSIONHISTORY}
  • + *
  • {@link NameConstants#JCR_BASEVERSION}
  • + *
  • {@link NameConstants#JCR_PREDECESSORS}
  • + *
  • {@link NameConstants#JCR_ISCHECKEDOUT}
  • + *
+ */ +public class VersionImporter extends DefaultProtectedPropertyImporter +{ + private final List _propertyNames = Arrays.asList(NameConstants.JCR_VERSIONHISTORY, NameConstants.JCR_BASEVERSION, NameConstants.JCR_PREDECESSORS, NameConstants.JCR_ISCHECKEDOUT); + private VersionImporterModifier _versionImporterModifier; + + @Override + public boolean init(JackrabbitSession aSession, NamePathResolver aResolver, boolean aIsWorkspaceImport, int aUuidBehavior, ReferenceChangeTracker aReferenceTracker) + { + _versionImporterModifier = new VersionImporterModifier(); + return super.init(aSession, aResolver, aIsWorkspaceImport, aUuidBehavior, aReferenceTracker); + } + + // -----------------------------------------< ProtectedPropertyImporter >--- + @Override + public boolean handlePropInfo(NodeImpl parent, PropInfo protectedPropInfo, QPropertyDefinition def) throws RepositoryException + { + if (parent.isNodeType(NameConstants.MIX_VERSIONABLE)) + { + Name propName = protectedPropInfo.getName(); + if (_propertyNames.contains(propName)) + { + // convert serialized values to Value objects + Value[] va = protectedPropInfo.getValues(protectedPropInfo.getTargetType(def), resolver); + + // set version properties + if (propName.equals(NameConstants.JCR_VERSIONHISTORY)) + { + if (referencedNodeExists(va[0])) + { + _versionImporterModifier.setVersionHistoryProperty(parent, va[0]); + return true; + } + } + else if (propName.equals(NameConstants.JCR_BASEVERSION)) + { + if (referencedNodeExists(va[0])) + { + _versionImporterModifier.setBaseVersionProperty(parent, va[0]); + return true; + } + } + else if (propName.equals(NameConstants.JCR_PREDECESSORS)) + { + Value[] existings = referencedNodesExist(va); + if (existings != null) + { + _versionImporterModifier.setPredecessorsProperty(parent, existings); + return true; + } + } + else if (propName.equals(NameConstants.JCR_ISCHECKEDOUT)) + { + _versionImporterModifier.setIsCheckedOutProperty(parent, va[0]); + return true; + } + } + } + + return false; + } + + //------------------------------------------------------------< private >--- + /** + * Test is the node referenced by the value exists. + * + * @param value + * @throws RepositoryException + */ + private boolean referencedNodeExists(Value value) throws RepositoryException + { + try + { + session.getNodeByIdentifier(value.getString()); + return true; + } + catch (ItemNotFoundException e) + { + return false; + } + } + + /** + * Filter values referencing an existing node only. + * + * @param values values to be filtered + * @return An array of the filtered values. + * @throws RepositoryException + */ + private Value[] referencedNodesExist(Value[] values) throws RepositoryException + { + List existings = new ArrayList(); + for (int i = 0; i < values.length; i++) + { + if (referencedNodeExists(values[0])) + { + existings.add(values[0]); + } + } + + return existings.isEmpty() ? null : existings.toArray(new Value[existings.size()]); + } +} Index: main/plugin-cms/src/org/ametys/cms/io/importers/ImportReferenceTracker.java =================================================================== --- main/plugin-cms/src/org/ametys/cms/io/importers/ImportReferenceTracker.java (revision 0) +++ main/plugin-cms/src/org/ametys/cms/io/importers/ImportReferenceTracker.java (revision 0) @@ -0,0 +1,132 @@ +/* + * Copyright 2012 Anyware Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ametys.cms.io.importers; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.jcr.Property; +import javax.jcr.PropertyType; + +/** + * Helper class that tracks remapped uuid's and imported reference + * that might need adjusting at the end of the import. + */ +public class ImportReferenceTracker +{ + /** + * Map that tracks nodes of interest for which a newly identifier (uuid) may have been + * assigned during the import. + * Tracking should be done for node extending one of these jcr:primaryType : + *
    + *
  • ametys:defaultContent (can have a workflowRef)
  • + *
  • oswf:entry (have a contentRef)
  • + *
+ */ + protected Map _newUUIDTracker = new HashMap(); + + /** Map that tracks the imported 'ametys-internal:workflowRef' properties */ + private final List _workflowRefsTracker = new ArrayList(); + + /** Map that tracks the imported 'ametys-internal:contentRef' properties */ + private final List _contentRefsTracker = new ArrayList(); + + /** + * Map that tracks the other imported properties of type + * {@link PropertyType#REFERENCE} or {@link PropertyType#WEAKREFERENCE} or {@link PropertyType#PATH} + * for later processing. + */ + private final List _otherRefsTracker = new ArrayList(); + + /** + * Store the given uuid mapping for later processing. + * @param oldUUID old node uuid + * @param newUUID new node uuid + */ + public void putUUIDChange(String oldUUID, String newUUID) + { + _newUUIDTracker.put(oldUUID, newUUID); + } + + /** + * Add a new property to be tracked. + * Should be an 'ametys-internal:workflowRef'. + * @param property + */ + public void tracksWorkflowRef(Property property) + { + _workflowRefsTracker.add(property); + } + + /** + * Add a new property to be tracked. + * Should be an 'ametys-internal:contentRef' + * @param property The JCR property object + */ + public void tracksContentRef(Property property) + { + _contentRefsTracker.add(property); + } + + /** + * Add a new property to be tracked. + * @param property The JCR property object + */ + public void tracksOthersRef(Property property) + { + _otherRefsTracker.add(property); + } + + /** + * Returns _newUUIDTracker + * @return _newUUIDTracker + */ + public Map getUUIDChanges() + { + return _newUUIDTracker; + } + + /** + * Returns the list of the tracked properties that a related to the workflows. + * @return the tracked properties (contentRef or workflowRef) + */ + public List getWfTrackedRefs() + { + List refs = new ArrayList(_workflowRefsTracker); + refs.addAll(_contentRefsTracker); + return refs; + } + + /** + * Returns the others tracked references. + * @return list of {@link Property} + */ + public List getOtherRefs() + { + return _otherRefsTracker; + } + + /** Resets all internal state. */ + public void clear() + { + _newUUIDTracker.clear(); + _workflowRefsTracker.clear(); + _contentRefsTracker.clear(); + _otherRefsTracker.clear(); + } +} Index: main/plugin-cms/src/org/ametys/cms/io/importers/ResourcesImporter.java =================================================================== --- main/plugin-cms/src/org/ametys/cms/io/importers/ResourcesImporter.java (revision 0) +++ main/plugin-cms/src/org/ametys/cms/io/importers/ResourcesImporter.java (revision 0) @@ -0,0 +1,139 @@ +/* + * Copyright 2012 Anyware Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ametys.cms.io.importers; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import javax.jcr.ImportUUIDBehavior; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.commons.lang.StringUtils; +import org.apache.jackrabbit.JcrConstants; +import org.xml.sax.SAXException; + +import org.ametys.plugins.explorer.resources.jcr.JCRResourcesCollectionFactory; + +/** + * Ametys resources importer using the JCR Import. + */ +public class ResourcesImporter extends JcrImporter +{ + /** Avalon role. */ + public static final String ROLE = ResourcesImporter.class.getName(); + + /** input resource node data list property */ + public static final String IMPORT_RESOURCE_NODE_LIST_PROPERTY = "nodeDataList"; + + /** input binary resource data list property */ + public static final String IMPORT_BINARY_RESOURCE_LIST_PROPERTY = "binaryDataList"; + + /** + * Import ametys resources + * @param importedNode + * @param nodeDataList + * @param binaryDataList + * @throws RepositoryException + * @throws IOException + * @throws SAXException + */ + public void doImport(Node importedNode, List nodeDataList, List binaryDataList) throws RepositoryException, SAXException, IOException + { + final Session session = importedNode.getSession(); + + // An exception will be thrown if there is an identifier collision. + final int uuidBehavior = ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW; + + // Import resource collection nodes. + for (ResourceNodeImportData nodeData : nodeDataList) + { + String relPath = nodeData._parentRelPath; + Node parentNode = StringUtils.isEmpty(relPath) ? importedNode : importedNode.getNode(nodeData._parentRelPath); + + importNode(parentNode, uuidBehavior, nodeData._inputStream, null); + } + + session.save(); + + // Import binary resources. + final String endResourcePath = '/' + JcrConstants.JCR_CONTENT; + final String dataPropertyName = JcrConstants.JCR_DATA; + for (BinaryResourceImportData binaryData : binaryDataList) + { + Node resourceNode = importedNode.getNode(binaryData._resourceRelPath + endResourcePath); + importBinaryProperty(resourceNode, dataPropertyName, binaryData._binaryStream); + + session.save(); + } + } + + /** + * Import data structure used to execute the import. An instance of this + * class corresponds to an ametys:resources-collection to + * import. + * @see JCRResourcesCollectionFactory#RESOURCESCOLLECTION_NODETYPE + */ + public static class ResourceNodeImportData + { + /** + * The JCR path to the node into which the import will be done. The path + * is relative to the root imported node (site node, odf node...) + */ + protected String _parentRelPath; + /** Inputstream consuming XML corresponding to the node to be imported */ + protected InputStream _inputStream; + + /** + * Ctor + * @param parentPath + * @param inputStream + */ + public ResourceNodeImportData(String parentPath, InputStream inputStream) + { + _parentRelPath = parentPath; + _inputStream = inputStream; + } + } + + /** + * Import data structure for binary resources. + */ + public static class BinaryResourceImportData + { + /** + * The JCR path of the resource node that represents the binary data to + * be imported. The path is relative to the root imported node (site + * node, odf node...) + */ + protected String _resourceRelPath; + /** Inputstream consuming the binary data to import */ + protected InputStream _binaryStream; + + /** + * Ctor + * @param resourcePath + * @param binaryStream + */ + public BinaryResourceImportData(String resourcePath, InputStream binaryStream) + { + _resourceRelPath = resourcePath; + _binaryStream = binaryStream; + } + } +} Index: main/plugin-cms/src/org/ametys/cms/io/importers/AmetysImporter.java =================================================================== --- main/plugin-cms/src/org/ametys/cms/io/importers/AmetysImporter.java (revision 0) +++ main/plugin-cms/src/org/ametys/cms/io/importers/AmetysImporter.java (revision 0) @@ -0,0 +1,284 @@ +/* + * Copyright 2012 Anyware Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ametys.cms.io.importers; + +import java.util.Date; +import java.util.Iterator; +import java.util.Map; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.PathNotFoundException; +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.Workspace; +import javax.jcr.version.Version; +import javax.jcr.version.VersionHistory; +import javax.jcr.version.VersionIterator; +import javax.jcr.version.VersionManager; + +import org.apache.avalon.framework.logger.AbstractLogEnabled; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.spi.Path; + +import org.ametys.cms.io.IOUtils; + +/** + * Abstract Ametys Importer. + * Ametys Importers (like SiteImporter, ODFImporter etc...) should extend this class. + */ +public abstract class AmetysImporter extends AbstractLogEnabled +{ + /** + * Adjust references collected during an import. This method try to ensure + * referential integrity. A diagnosis is also made for references that do + * not enforce referential integrity (weak reference, path..) when the value + * do not point to an existing node. + * + * @param importedNode + * @param referenceTracker + * @throws RepositoryException + */ + protected void _adjustReferenceProperties(Node importedNode, ImportReferenceTracker referenceTracker) throws RepositoryException + { + final Session session = importedNode.getSession(); + + // Adjust contentRef / workflowRef references. + Map uuidChanges = referenceTracker.getUUIDChanges(); + for (Property reference : referenceTracker.getWfTrackedRefs()) + { + String original = reference.getString(); + String adjusted = uuidChanges.get(original); + if (adjusted != null) + { + try + { + Node nodeToBeReferenced = session.getNodeByIdentifier(adjusted); + reference.setValue(nodeToBeReferenced); + } + catch (ItemNotFoundException infe) + { + String msg = "Expecting to find a referenced node with identifier '" + adjusted + "' but did not." + + "\nIts original property value was '" + original + "'"; + getLogger().error(msg); + throw infe; + } + } + } + + // Process external references. + for (Property property : referenceTracker.getOtherRefs()) + { + _analyzeProperty(importedNode, property); + } + + referenceTracker.clear(); + } + + private void _analyzeProperty(Node node, Property property) throws RepositoryException + { + if (property.isMultiple()) + { + for (Value value : property.getValues()) + { + _analyzeProperty(node, property, value); + } + } + else + { + _analyzeProperty(node, property, property.getValue()); + } + } + + private void _analyzeProperty(Node node, Property property, Value value) throws RepositoryException + { + int propType = property.getType(); + switch (propType) + { + case PropertyType.REFERENCE: + case PropertyType.WEAKREFERENCE: + _analyzeReferenceProperty(node, property, value); + break; + case PropertyType.PATH: + _analyzePathProperty(node, property, value); + break; + default: + // should not happen. + getLogger().warn("unexpected property type"); + } + } + + private void _analyzeReferenceProperty(Node node, Property property, Value value) throws RepositoryException + { + try + { + node.getSession().getNodeByIdentifier(value.getString()); + } + catch (ItemNotFoundException e) + { + String type = property.getType() == PropertyType.REFERENCE ? PropertyType.TYPENAME_REFERENCE : PropertyType.TYPENAME_WEAKREFERENCE; + String msg = "Node '" + property.getParent().getName() + "' has a property of type '" + type + "' with value '" + value.getString() + + "' and the referenced node no longer exists."; + getLogger().warn(msg, e); + + if (PropertyType.TYPENAME_REFERENCE.equals(type)) + { + // Currently removes the reference and log. + String removeMsg = "Property '" + property.getName() + "' referencing an external item has been removed to preserve referential integrity." + + "\nProperty path was : '" + property.getPath() + "'"; + getLogger().warn(removeMsg); + property.remove(); + + } + } + } + + private void _analyzePathProperty(Node node, Property property, Value value) throws RepositoryException + { + Session session = node.getSession(); + if (!(session instanceof SessionImpl)) + { + String msg = "Cannot test if a property of type '" + PropertyType.TYPENAME_PATH + " of the node '" + property.getParent().getName() + + "' reference an item which is outside the scope of the imported node."; + getLogger().warn(msg); + return; + } + + String path = value.getString(); + Path p = ((SessionImpl) session).getQPath(path); + + boolean absolute = p.isAbsolute(); + // Path property could either reference a Node or a Property + try + { + if (absolute) + { + session.getNode(path); + } + else + { + property.getParent().getNode(path); + } + } + catch (PathNotFoundException e) + { + try + { + if (absolute) + { + session.getProperty(path); + } + else + { + property.getParent().getProperty(path); + } + } + catch (PathNotFoundException e2) + { + // Currenlty, only logs a warning. + String msg = "Node '" + property.getParent().getName() + "' has a property of type '" + PropertyType.TYPENAME_PATH + "' with value '" + value.getString() + + "' and the referenced item no longer exists."; + getLogger().warn(msg, e); + return; + } + } + } + + /** + * Adjust versionable nodes after an import. + * @param importedNode + * @param exportDate + * @throws RepositoryException + */ + protected void _adjustVersionableNodes(Node importedNode, Date exportDate) throws RepositoryException + { + Workspace importWorkspace = importedNode.getSession().getWorkspace(); + VersionManager versionManager = importWorkspace.getVersionManager(); + + // Currently, versionable nodes in Ametys are ametys:content and ametys:resource. + NodeIterator nodeIterator = IOUtils.retrieveVersionableNodes(importedNode); + + while (nodeIterator.hasNext()) + { + Node node = nodeIterator.nextNode(); + String path = node.getPath(); + VersionHistory versionHistory = versionManager.getVersionHistory(path); + + _removeNewerVersionsSinceExport(versionHistory, exportDate); + + // Create a new version if only the root version exists. + // It happens when the export/import is done in a different repository. + _createNewVersionIfNecessary(versionManager, path); + + // Additional processing + _additionalAdjustementForVersionableNode(node, versionHistory); + } + } + + /** + * Additional processing on versionable node. + * @param node the node to process + * @param versionHistory the version history bound to this node + * @throws RepositoryException + */ + protected void _additionalAdjustementForVersionableNode(Node node, VersionHistory versionHistory) throws RepositoryException + { + // Nothing to do + } + + private void _removeNewerVersionsSinceExport(VersionHistory versionHistory, Date exportDate) throws RepositoryException + { + if (exportDate == null) + { + return; + } + + boolean newerThanExport = true; + Iterator versionsIterator = IOUtils.getVersionSortedByCreatedDesc(versionHistory); + final String rootVersionName = versionHistory.getRootVersion().getName(); + while (versionsIterator.hasNext() && newerThanExport) + { + Version version = versionsIterator.next(); + + if (version.getCreated().getTime().before(exportDate)) + { + newerThanExport = false; + } + // Remove newer version, expect root version. + else if (!rootVersionName.equals(version.getName())) + { + versionHistory.removeVersion(version.getName()); + } + } + } + + private void _createNewVersionIfNecessary(VersionManager versionManager, String nodeAbsPath) throws RepositoryException + { + // Do a checkpoint if there is no initial version for this node. (rootVersion excluded) + VersionHistory versionHistory = versionManager.getVersionHistory(nodeAbsPath); + VersionIterator versions = versionHistory.getAllVersions(); + + versions.nextVersion(); // root version. + if (!versions.hasNext()) + { + versionManager.checkpoint(nodeAbsPath); + } + } +} Index: main/plugin-cms/src/org/ametys/cms/io/handlers/ImportAmetysObjectHandler.java =================================================================== --- main/plugin-cms/src/org/ametys/cms/io/handlers/ImportAmetysObjectHandler.java (revision 0) +++ main/plugin-cms/src/org/ametys/cms/io/handlers/ImportAmetysObjectHandler.java (revision 0) @@ -0,0 +1,318 @@ +/* + * Copyright 2012 Anyware Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ametys.cms.io.handlers; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.ValueFactory; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.value.ValueHelper; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import org.ametys.cms.io.IOConstants; +import org.ametys.cms.io.IOHandler; +import org.ametys.cms.io.exporters.JcrExporter; +import org.ametys.cms.io.importers.ImportReferenceTracker; + +/** + * "Proxy" handler used to import an ametys object (site, odf..). + * Collects useful information that will be processed later. + */ +public class ImportAmetysObjectHandler extends IOHandler +{ + /** Ametys internal workflowRef property */ + protected static final int IN_WORKFLOW_ID_PROPERTY = 12; + + /** Ametys internal export date property */ + protected static final int IN_EXPORT_DATE_PROPERTY = 13; + + /** uuid property name */ + protected static final String _UUID_PROPERTY_NAME = "uuid"; + + /** new workflow id property */ + protected static final String _NEW_WORKFLOW_ID = "newWorkflowId"; + + /** export date property */ + protected static final String _EXPORT_DATE_STRING = "exportDateStr"; + + /** references property */ + protected static final String _REFERENCES_PROPERTY = "references"; + + /** The reference tracker to populate during import */ + protected ImportReferenceTracker _referenceTracker; + + /** Mapping of old oswf:entry id to new oswf:entry id */ + protected Map _idMapping = new HashMap(); + + /** Export date retrieved during the import */ + protected Date _exportDate; + + /** Name of the imported node */ + protected String _importedNodeName; + + /** List of property types that denote a reference */ + protected final List _referenceTypes = Arrays.asList( + PropertyType.TYPENAME_REFERENCE, + PropertyType.TYPENAME_WEAKREFERENCE, + PropertyType.TYPENAME_PATH); + + /** + * Ctor inheritance + * @param parentNode the session in which the import is done + * @param referenceTracker + * @param oswfEntryIdMapping + */ + public ImportAmetysObjectHandler(Node parentNode, ImportReferenceTracker referenceTracker, Map oswfEntryIdMapping) + { + super(new DefaultHandler(), parentNode); + _referenceTracker = referenceTracker; + _idMapping = oswfEntryIdMapping; + } + + @Override + protected void _onNodeStart(Attributes atts) + { + // _importedNodeName is null when SAX'ing the root node which is the imported node. + if (_importedNodeName == null) + { + _importedNodeName = atts.getValue(IOConstants.SV_NAME); + } + } + + @Override + protected void _onPropertyStart(Attributes atts) + { + String name = atts.getValue(IOConstants.SV_NAME); + String type = atts.getValue(IOConstants.SV_TYPE); + + // Collecting the jcr:uuid of the current node + if (JcrConstants.JCR_UUID.equals(name)) + { + _state = IN_JCR_UUID_PROPERTY; + } + else if (IOConstants.WORKFLOW_ID_PROPERTY.equals(name)) + { + _state = IN_WORKFLOW_ID_PROPERTY; + } + else if (JcrExporter.EXPORT_DATE_PROPERTY.equals(name)) + { + _state = IN_EXPORT_DATE_PROPERTY; + } + else if (_referenceTypes.contains(type) && !name.startsWith(Name.NS_JCR_PREFIX + ':')) + { + Properties props = _nodeStack.peek(); + + List references = (List) props.get(_REFERENCES_PROPERTY); + if (references == null) + { + references = new ArrayList(); + props.put(_REFERENCES_PROPERTY, references); + } + references.add(name); + } + } + + @Override + protected void _onValueStart(Attributes atts) + { + switch (_state) + { + case IN_JCR_UUID_PROPERTY: + case IN_WORKFLOW_ID_PROPERTY: + // Set the scan characters flag + _flags |= SCAN_CHARACTERS; + break; + case IN_EXPORT_DATE_PROPERTY: + // Set the scan characters flag + _flags |= SCAN_CHARACTERS; + + // Stop forwarding events + _flags &= ~(FORWARD_STARTS | FORWARD_ENDS | FORWARD_CHARACTERS); + break; + default: + break; + } + } + + @Override + protected void _onNodeEnd() throws SAXException + { + Properties props = _nodeStack.peek(); + + // 1 - Retrieves imported node from path + String relPath = (String) props.get(PATH_PROPERTY_NAME); + if (relPath == null) + { + return; + } + + try + { + Node node = _parentNode.getNode(relPath); + + // 2 - Populates reference tracker + List references = (List) props.get(_REFERENCES_PROPERTY); + if (references != null && !references.isEmpty()) + { + for (String refProperty : references) + { + Property property = node.getProperty(refProperty); + _referenceTracker.tracksOthersRef(property); + } + } + + // 3 - Analyze primary type + if (!node.isNodeType(IOConstants.DEFAULT_CONTENT_NODETYPE)) + { + return; + } + + // 4 - Checks if its uuid has changed + String oldUUID = (String) props.get(_UUID_PROPERTY_NAME); + if (oldUUID == null) + { + String msg = "Expecting an UUID property for this node."; + _logger.error(msg); + throw new SAXException(msg); + } + + String newUUID = node.getIdentifier(); + if (!newUUID.equals(oldUUID)) + { + _referenceTracker.putUUIDChange(oldUUID, newUUID); + } + + // 5 - Update the workflow id property + Long newWorkflowId = (Long) props.get(_NEW_WORKFLOW_ID); + if (newWorkflowId == null) + { + String msg = "Expecting a mapped entry for the '" + IOConstants.WORKFLOW_ID_PROPERTY + "' property."; + _logger.warn(msg); + } + else + { + node.setProperty(IOConstants.WORKFLOW_ID_PROPERTY, newWorkflowId); + } + + // 6 - Add reference to track + try + { + Property workflowRef = node.getProperty(IOConstants.WORKFLOW_REF_PROPERTY_NAME); + _referenceTracker.tracksWorkflowRef(workflowRef); + } + catch (RepositoryException e) + { + String msg = "Expecting a '" + IOConstants.WORKFLOW_REF_PROPERTY_NAME + "' property for this node."; + _logger.warn(msg, e); + } + } + catch (RepositoryException e) + { + String msg = "Error during the post processing step of the import of a node."; + _logger.error(msg, e); + throw new SAXException(msg, e); + } + } + + @Override + protected void _onPropertyEnd() + { + switch (_state) + { + case IN_JCR_UUID_PROPERTY: + case IN_WORKFLOW_ID_PROPERTY: + _state = IN_NODE; + break; + case IN_EXPORT_DATE_PROPERTY: + _state = IN_NODE; + // Start forwarding events + _flags |= FORWARD_STARTS | FORWARD_ENDS | FORWARD_CHARACTERS; + break; + default: + break; + } + } + + @Override + protected void _onValueEnd(String value) throws SAXException + { + switch (_state) + { + case IN_JCR_UUID_PROPERTY: + _nodeStack.peek().put(_UUID_PROPERTY_NAME, value); + break; + case IN_WORKFLOW_ID_PROPERTY: + Long oldWorkflowId = Long.valueOf(value); + Long newWorkflowId = _idMapping.get(oldWorkflowId); + _nodeStack.peek().put(_NEW_WORKFLOW_ID, newWorkflowId); + break; + case IN_EXPORT_DATE_PROPERTY: + try + { + ValueFactory vf = _parentNode.getSession().getValueFactory(); + _exportDate = ValueHelper.convert(value, PropertyType.DATE, vf).getDate().getTime(); + } + catch (RepositoryException e) + { + String msg = "Unable to convert export date property"; + _logger.error(msg, e); + throw new SAXException(msg, e); + } + break; + default: + break; + } + } + + /** + * Retrieves the exportDate. + * @return the exportDate + */ + public Date getExportDate() + { + return _exportDate; + } + + /** + * Retrieves the imported node. + * Should only be called after the import has been done. + * @return The imported node. + * @throws RepositoryException + */ + public Node getImportedNode() throws RepositoryException + { + if (_importedNodeName != null) + { + return _parentNode.getNode(_importedNodeName); + } + + return null; + } +} Index: main/plugin-cms/src/org/ametys/cms/io/handlers/ExportAmetysObjectHandler.java =================================================================== --- main/plugin-cms/src/org/ametys/cms/io/handlers/ExportAmetysObjectHandler.java (revision 0) +++ main/plugin-cms/src/org/ametys/cms/io/handlers/ExportAmetysObjectHandler.java (revision 0) @@ -0,0 +1,509 @@ +/* + * Copyright 2012 Anyware Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ametys.cms.io.handlers; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import javax.jcr.Item; +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; + +import org.apache.commons.lang.StringUtils; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.xml.sax.Attributes; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; + +import org.ametys.cms.io.IOConstants; +import org.ametys.cms.io.IOHandler; +import org.ametys.cms.io.exporters.LogExporter; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ListMultimap; + +/** + * "Proxy" handler used to export an ametys object. + */ +public class ExportAmetysObjectHandler extends IOHandler +{ + /** In skipped node */ + protected static final int IN_SKIPPED_NODE = 1; + + /** Ametys workflowRef property */ + protected static final int IN_WORKFLOWREF_PROPERTY = 12; + + /** references property */ + protected static final String _REFERENCES_PROPERTY = "references"; + + /** Set of the collected workflow references */ + protected final Set _workflowRefs = new HashSet(); + + /** MultiMap that maps node relative path to the name of their binary properties */ + protected final ListMultimap _binaryProperties = ArrayListMultimap.create(); + + /** Set of the collected ressources by path */ + protected final Set _resources = new HashSet(); + + /** MultiMap of the collected external references, keyed by node identifier */ + protected final ListMultimap _externalReferences = ArrayListMultimap.create(); + + /** List of property types that denote a reference */ + protected final List _referenceTypes = Arrays.asList( + PropertyType.TYPENAME_REFERENCE, + PropertyType.TYPENAME_WEAKREFERENCE, + PropertyType.TYPENAME_PATH); + + /** List of node names that may not be exported, must be populated in the constructor if needed */ + protected final List _nodesToSkip = new ArrayList(); + + /** An internal counter helper */ + protected int _cpt; + + /** + * Ctor inheritance + * @param contentHandler The {@link ContentHandler} to be wrapped. + * @param parentNode The node from which the import/export is relative + */ + public ExportAmetysObjectHandler(final ContentHandler contentHandler, Node parentNode) + { + super(contentHandler, parentNode); + } + + @Override + protected void _onNodeStart(Attributes atts) throws SAXException + { + switch (_state) + { + case IN_NODE: + String name = atts.getValue(IOConstants.SV_NAME); + if (_nodesToSkip.contains(name)) + { + boolean skipExport = _onNodeToSkip(name); + + if (skipExport) + { + _state = IN_SKIPPED_NODE; + _cpt++; + + // Stop forwarding events + _flags &= ~(FORWARD_STARTS | FORWARD_ENDS | FORWARD_CHARACTERS); + } + } + break; + case IN_SKIPPED_NODE: + _cpt++; + break; + default: + break; + } + } + + /** + * This method is used to confirm that a node must be skipped. Specific processing can also be done here. + * @param name The name of the node + * @return true to confirme that the node will be skipped (ie. not exported). + * @throws SAXException + */ + protected boolean _onNodeToSkip(String name) throws SAXException + { + return true; + } + + @Override + protected void _onPropertyStart(Attributes atts) + { + switch (_state) + { + case IN_NODE: + String type = atts.getValue(IOConstants.SV_TYPE); + String name = atts.getValue(IOConstants.SV_NAME); + + if (IOConstants.WORKFLOW_REF_PROPERTY_NAME.equals(name)) + { + _state = IN_WORKFLOWREF_PROPERTY; + } + else if (PropertyType.TYPENAME_BINARY.equals(type)) + { + String path = (String) _nodeStack.peek().get(PATH_PROPERTY_NAME); + _binaryProperties.put(path, name); + } + else if (_referenceTypes.contains(type) && !name.startsWith(Name.NS_JCR_PREFIX + ':')) + { + Properties props = _nodeStack.peek(); + + List references = (List) props.get(_REFERENCES_PROPERTY); + if (references == null) + { + references = new ArrayList(); + props.put(_REFERENCES_PROPERTY, references); + } + references.add(name); + } + break; + default: + break; + } + } + + @Override + protected void _onValueStart(Attributes atts) + { + switch (_state) + { + case IN_WORKFLOWREF_PROPERTY: + // Set the scan characters flag + _flags |= SCAN_CHARACTERS; + break; + default: + break; + } + } + + @Override + protected void _onNodeEnd() throws SAXException + { + switch (_state) + { + case IN_NODE: + // Populate external references + Properties props = _nodeStack.peek(); + List references = (List) props.get(_REFERENCES_PROPERTY); + String path = (String) props.get(PATH_PROPERTY_NAME); + try + { + path = StringUtils.removeStart(path, _parentNode.getName() + '/'); + _populateExternalReferences(references, path); + } + catch (RepositoryException e) + { + String msg = "Error during the export of the node '" + props.get(NAME_PROPERTY_NAME) + "'."; + _logger.error(msg, e); + throw new SAXException(msg, e); + } + break; + case IN_SKIPPED_NODE: + _cpt--; + + if (_cpt == 0) + { + _state = IN_NODE; + + // Start forwarding events + _flags |= FORWARD_STARTS | FORWARD_ENDS | FORWARD_CHARACTERS; + } + break; + default: + break; + } + } + + /** + * Populate external references multimap. + * @param referenceProperties + * @param relPath Path to the current node, relative to the export base node. + * @throws RepositoryException + */ + private void _populateExternalReferences(List referenceProperties, String relPath) throws RepositoryException + { + if (referenceProperties == null || referenceProperties.isEmpty()) + { + return; + } + + Node node = _parentNode.getNode(relPath); + for (String refProperty : referenceProperties) + { + Property property = node.getProperty(refProperty); + _addExternalReferences(node, property); + } + } + + + /** + * Add external references + * @param node + * @param property + * @throws ValueFormatException + * @throws RepositoryException + */ + private void _addExternalReferences(Node node, Property property) throws RepositoryException + { + if (property.isMultiple()) + { + for (Value value : property.getValues()) + { + _addExternalReference(node, property, value); + } + } + else + { + _addExternalReference(node, property, property.getValue()); + } + } + + /** + * Add external reference + * @param node + * @param property + * @param value + * @throws RepositoryException + */ + private void _addExternalReference(Node node, Property property, Value value) throws RepositoryException + { + int propType = property.getType(); + switch (propType) + { + case PropertyType.REFERENCE: + _addExternalReferenceReferenceType(node, property, value); + break; + case PropertyType.WEAKREFERENCE: + _addExternalReferenceWeakReferenceType(node, property, value); + break; + case PropertyType.PATH: + _addExternalReferencePathType(node, property, value); + break; + default: + // should not happen. + _logger.warn("unexpected property type"); + } + } + + /** + * Add an external reference of type REFERENCE to the + * {@link #_externalReferences} MultiMap if needed. + * + * @param node + * @param property + * @param value + * @throws RepositoryException + */ + private void _addExternalReferenceReferenceType(Node node, Property property, Value value) throws RepositoryException + { + Node referencedNode = null; + try + { + referencedNode = node.getSession().getNodeByIdentifier(value.getString()); + } + catch (ItemNotFoundException e) + { + // Should not occurs + String msg = "Node '" + node.getName() + "' has a property of type '" + PropertyType.TYPENAME_REFERENCE + "' with value '" + value.getString() + + "' but the referenced node do not exists."; + _logger.error(msg, e); + throw e; + } + + _populateExternalReferences(node, property, value, PropertyType.TYPENAME_REFERENCE, referencedNode); + } + + /** + * Add an external reference of type WEAKREFERENCE to the + * {@link #_externalReferences} MultiMap if needed. + * + * @param node + * @param property + * @param value + * @throws RepositoryException + * @throws RepositoryException + */ + private void _addExternalReferenceWeakReferenceType(Node node, Property property, Value value) throws RepositoryException + { + Node referencedNode = null; + + try + { + referencedNode = node.getSession().getNodeByIdentifier(value.getString()); + } + catch (ItemNotFoundException e) + { + String msg = "Node '" + node.getName() + "' has a property of type '" + PropertyType.TYPENAME_WEAKREFERENCE + "' with value '" + value.getString() + + "' and the referenced node no longer exists."; + _logger.warn(msg, e); + return; + } + + _populateExternalReferences(node, property, value, PropertyType.TYPENAME_WEAKREFERENCE, referencedNode); + } + + /** + * Add an external reference of type PATH to the + * {@link #_externalReferences} MultiMap if needed. + * + * @param node + * @param property + * @param value + * @throws RepositoryException + * @throws RepositoryException + */ + private void _addExternalReferencePathType(Node node, Property property, Value value) throws RepositoryException + { + Item referencedItem = null; + + Session session = node.getSession(); + if (!(session instanceof SessionImpl)) + { + String msg = "Cannot test if a property of type '" + PropertyType.TYPENAME_PATH + " of the node '" + node.getName() + + "' reference an item which is outside the scope of the site."; + _logger.warn(msg); + return; + } + + String path = value.getString(); + Path p = ((SessionImpl) session).getQPath(path); + + boolean absolute = p.isAbsolute(); + try + { + referencedItem = absolute ? session.getNode(path) : property.getParent().getNode(path); + } + catch (PathNotFoundException e) + { + try + { + referencedItem = absolute ? session.getProperty(path) : property.getParent().getProperty(path); + } + catch (PathNotFoundException e2) + { + String msg = "Node '" + node.getName() + "' has a property of type '" + PropertyType.TYPENAME_PATH + "' with value '" + value.getString() + + "' and the referenced item no longer exists."; + _logger.warn(msg, e); + return; + } + } + + _populateExternalReferences(node, property, value, PropertyType.TYPENAME_PATH, referencedItem); + } + + /** + * Populate the external references multimap with a new value. + * @param node + * @param property + * @param value + * @param referencedItem + * @param refType + * @throws RepositoryException + */ + private void _populateExternalReferences(Node node, Property property, Value value, String refType, Item referencedItem) throws RepositoryException + { + // To be considered as external, referenced item should not belong to the site. + if (_isExternal(referencedItem)) + { + Properties props = new Properties(); + props.put(LogExporter.EXTERNAL_REF_PROPERTY_NODE_NAME, node.getName()); + props.put(LogExporter.EXTERNAL_REF_PROPERTY_NODE_PATH, node.getPath()); + props.put(LogExporter.EXTERNAL_REF_PROPERTY_NAME, property.getName()); + props.put(LogExporter.EXTERNAL_REF_PROPERTY_TYPE, refType); + props.put(LogExporter.EXTERNAL_REF_PROPERTY_VALUE, value.getString()); + _externalReferences.put(node.getIdentifier(), props); + } + } + + /** + * This method must indicate is the referenced item passed as an argument is + * external (out of the scope of the export). + * + * @param referencedItem The item + * @return true to confirme that the node will be skipped (ie. not + * exported). + * @throws RepositoryException + */ + protected boolean _isExternal(Item referencedItem) throws RepositoryException + { + return false; + } + + @Override + protected void _onPropertyEnd() + { + switch (_state) + { + case IN_WORKFLOWREF_PROPERTY: + _state = IN_NODE; + break; + default: + break; + } + } + + @Override + protected void _onValueEnd(String value) + { + switch (_state) + { + case IN_WORKFLOWREF_PROPERTY: + if (value != null) + { + _workflowRefs.add(value); + } + break; + default: + break; + } + } + + // Getters + /** + * Retrieves the workflow identifiers. + * @return the workflow identifiers + */ + public Set getWorkflowRefs() + { + return _workflowRefs; + } + + /** + * Retrieves the binary properties. + * @return the binaryProperties + */ + public Map> getBinaryProperties() + { + return _binaryProperties.asMap(); + } + + /** + * Retrieves the resources. + * @return the resources + */ + public Set getResources() + { + return _resources; + } + + /** + * Retrieves the external references + * @return the externalReferences + */ + public Map> getExternalReferences() + { + return _externalReferences.asMap(); + } +} Index: main/plugin-cms/src/org/ametys/cms/io/handlers/ImportWorkflowHandler.java =================================================================== --- main/plugin-cms/src/org/ametys/cms/io/handlers/ImportWorkflowHandler.java (revision 0) +++ main/plugin-cms/src/org/ametys/cms/io/handlers/ImportWorkflowHandler.java (revision 0) @@ -0,0 +1,403 @@ +/* + * Copyright 2012 Anyware Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ametys.cms.io.handlers; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; + +import org.apache.cocoon.xml.AttributesImpl; +import org.apache.commons.lang.BooleanUtils; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import org.ametys.cms.io.IOConstants; +import org.ametys.cms.io.IOHandler; +import org.ametys.cms.io.IOUtils; +import org.ametys.cms.io.importers.ImportReferenceTracker; + +/** + * "Proxy" handler used to import a workflow. + * Import workflow entry with new (unused) id and collect information. + */ +public class ImportWorkflowHandler extends IOHandler +{ + /** In entry node state */ + protected static final int IN_ENTRY_NODE = 1; + + /** Do not forward event to superclass mode. */ + protected static final int _DO_NOT_FORWARD_EVENTS_MODE = -1; + + /** In oswf:id property of an entry node */ + protected static final int IN_ENTRY_NODE_ID_PROPERTY = 12; + + /** uuid property name */ + protected static final String _UUID_PROPERTY_NAME = "uuid"; + + /** imported node is an oswf:entry */ + protected static final String _IS_ENTRY_NODE_PROPERTY = "isEntryNode"; + + /** old entry id property */ + protected static final String _OLD_ENTRY_ID_PROPERTY = "oldEntryId"; + + /** Internal node depth counter */ + protected int _nodeCpt; + + /** The oswf:root workflow node */ + protected Node _rootWorkflowNode; + + /** The reference tracker to populate during import */ + protected ImportReferenceTracker _referenceTracker; + + /** Mapping of old entry id to new entry id */ + protected Map _idMapping = new HashMap(); + + /** The value of the next entry id property */ + protected long _currentEntryId; + + /** The JCR uuidBehavior used for the import */ + protected int _uuidBehavior; + + /** + * Ctor inheritance + * @param referenceTracker + * @param rootWorkflowNode + * @param uuidBehavior + * @param nextEntryId the value of the oswf root node next entry id property + */ + public ImportWorkflowHandler(Node rootWorkflowNode, int uuidBehavior, ImportReferenceTracker referenceTracker, long nextEntryId) + { + super(new DefaultHandler(), null); + _rootWorkflowNode = rootWorkflowNode; + _uuidBehavior = uuidBehavior; + _referenceTracker = referenceTracker; + _currentEntryId = nextEntryId - 1; + _state = _DO_NOT_FORWARD_EVENTS_MODE; + } + + @Override + public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException + { + if (IOConstants.SV_NODE.equals(qName)) + { + if (_state == _DO_NOT_FORWARD_EVENTS_MODE) + { + _state = IN_NODE; + _nodeCpt = 0; + _startWorkflowEntryImport(); + } + + _nodeCpt++; + } + + if (_state != _DO_NOT_FORWARD_EVENTS_MODE) + { + super.startElement(namespaceURI, localName, qName, atts); + } + } + + @Override + public void endElement(String namespaceURI, String localName, String qName) throws SAXException + { + if (_state != _DO_NOT_FORWARD_EVENTS_MODE) + { + super.endElement(namespaceURI, localName, qName); + + if (IOConstants.SV_NODE.equals(qName)) + { + _nodeCpt--; + + if (_nodeCpt == 0) + { + _state = _DO_NOT_FORWARD_EVENTS_MODE; + _endWorkflowEntryImport(); + } + } + } + } + + @Override + public void characters(char[] ch, int start, int length) throws SAXException + { + if (_state != _DO_NOT_FORWARD_EVENTS_MODE) + { + super.characters(ch, start, length); + } + } + + /** + * Starts the JCR import of a new workflow node (oswf:entry) + * @throws SAXException + */ + protected void _startWorkflowEntryImport() throws SAXException + { + // Increment oswf entry id. + _currentEntryId++; + + try + { + _parentNode = _getWorkflowEntryParentNode(_currentEntryId); + setContentHandler(_rootWorkflowNode.getSession().getImportContentHandler(_parentNode.getPath(), _uuidBehavior)); + } + catch (RepositoryException e) + { + String msg = "Fatal error when trying to import a new workflow entry with id '" + "" + "'."; + _logger.error(msg); + throw new SAXException(msg, e); + } + + startDocument(); // start workflow entry import + } + + /** + * Retrieves the parent node of a workflow node. + * Create the necessary nodes if not existing. + * @param entryId the id of the node + * @return The parent node (an oswf:hashSegment) + * @throws RepositoryException + */ + protected Node _getWorkflowEntryParentNode(Long entryId) throws RepositoryException + { + // Generate hash for this entry id. + List hashSegments = IOUtils.getHashedName(Long.toString(entryId)); + + // Balanced tree management + Node htree = _rootWorkflowNode; + for (String hashSegment : hashSegments) + { + if (htree.hasNode(hashSegment)) + { + // Go down to a leaf of the hash tree + htree = htree.getNode(hashSegment); + } + else + { + // Add a new leaf to the hash tree + htree = htree.addNode(hashSegment, IOConstants.OSWF_HTREE_NT); + } + } + + return htree; + } + + /** + * Ends the JCR import of a new workflow node (oswf:entry) + * @throws SAXException + */ + protected void _endWorkflowEntryImport() throws SAXException + { + endDocument(); // end of workflow entry import. + + setContentHandler(new DefaultHandler()); + _parentNode = null; + } + + @Override + protected void _onNodeStart(Attributes atts) throws SAXException + { + _state = IN_NODE; + + // In an entry node, dynamically change the value of the entry id. + String name = atts.getValue(IOConstants.SV_NAME); + if (name.startsWith(IOConstants.ENTRY_NODE_PREFIX)) + { + _state = IN_ENTRY_NODE; + + Properties props = _nodeStack.peek(); + props.put(_IS_ENTRY_NODE_PROPERTY, true); + + AttributesImpl newAtts = new AttributesImpl(atts); + String newName = IOConstants.ENTRY_NODE_PREFIX + _currentEntryId; + newAtts.setValue(atts.getIndex(IOConstants.SV_NAME), newName); + + // update path property (no need to count children, because oswf:entry name are uniques) + _path.pop(); + _path.push(newName); + props.put(PATH_PROPERTY_NAME, _getCurrentPath()); + props.put(NAME_PROPERTY_NAME, newName); + + Name svNodeName = NameConstants.SV_NODE; + sendStartElement(svNodeName.getNamespaceURI(), svNodeName.getLocalName(), IOConstants.SV_NODE, newAtts); + + // Do not forward the very next start event. + _flags |= DO_NOT_FORWARD_NEXT_START; + } + } + + @Override + protected void _onPropertyStart(Attributes atts) throws SAXException + { + String name = atts.getValue(IOConstants.SV_NAME); + + // Collecting the jcr:uuid of the current node + if ("jcr:uuid".equals(name)) + { + _state = IN_JCR_UUID_PROPERTY; + } + else if (IOConstants.ENTRY_NODE_ID_PROPERTY.equals(name) && _state == IN_ENTRY_NODE) + { + _state = IN_ENTRY_NODE_ID_PROPERTY; + } + } + + @Override + protected void _onValueStart(Attributes atts) throws SAXException + { + switch (_state) + { + case IN_JCR_UUID_PROPERTY: + // Set the scan characters flag + _flags |= SCAN_CHARACTERS; + break; + case IN_ENTRY_NODE_ID_PROPERTY: + Name svValueName = NameConstants.SV_VALUE; + sendStartElement(svValueName.getNamespaceURI(), svValueName.getLocalName(), IOConstants.SV_VALUE, atts); + + char[] entryId = Long.toString(_currentEntryId).toCharArray(); + sendCharacters(entryId, 0, entryId.length); + + // Set the scan characters and do not forward next start flags + _flags |= SCAN_CHARACTERS | DO_NOT_FORWARD_NEXT_START; + // Stop forwarding characters events + _flags &= ~FORWARD_CHARACTERS; + break; + default: + break; + } + } + + @Override + protected void _onNodeEnd() throws SAXException + { + Properties props = _nodeStack.peek(); + + // 1 - Retrieves imported node from path + String relPath = (String) props.get(PATH_PROPERTY_NAME); + if (relPath == null) + { + return; + } + + try + { + Node node = _parentNode.getNode(relPath); + + // 2 - Analyze primary type + if (!node.isNodeType(IOConstants.OSWF_ENTRY_NT)) + { + return; + } + + // 3 - Checks if its uuid has changed + String oldUUID = (String) props.get(_UUID_PROPERTY_NAME); + if (oldUUID == null) + { + String msg = "Expecting an UUID property for this node."; + _logger.error(msg); + throw new SAXException(msg); + } + + String newUUID = node.getIdentifier(); + if (!newUUID.equals(oldUUID)) + { + _referenceTracker.putUUIDChange(oldUUID, newUUID); + } + + // 4 - Add reference to track + try + { + Property contentRef = node.getProperty(IOConstants.CONTENT_REF_PROPERTY_NAME); + _referenceTracker.tracksContentRef(contentRef); + } + catch (RepositoryException e) + { + String msg = "Expecting a '" + IOConstants.CONTENT_REF_PROPERTY_NAME + "' property for this node."; + _logger.error(msg); + throw e; + } + + // 5 - Add the entry id mapping + Long oldEntryId = (Long) props.get(_OLD_ENTRY_ID_PROPERTY); + Long newEntryId = node.getProperty(IOConstants.ENTRY_NODE_ID_PROPERTY).getLong(); + _idMapping.put(oldEntryId, newEntryId); + } + catch (RepositoryException e) + { + String msg = "Error during the post processing step of the import of a node."; + _logger.error(msg, e); + throw new SAXException(msg, e); + } + } + + @Override + protected void _onPropertyEnd() throws SAXException + { + Boolean isEntryNode = (Boolean) _nodeStack.peek().get(_IS_ENTRY_NODE_PROPERTY); + if (BooleanUtils.isTrue(isEntryNode)) + { + _state = IN_ENTRY_NODE; + } + else + { + _state = IN_NODE; + } + } + + @Override + protected void _onValueEnd(String value) throws SAXException + { + switch (_state) + { + case IN_JCR_UUID_PROPERTY: + _nodeStack.peek().put(_UUID_PROPERTY_NAME, value); + break; + case IN_ENTRY_NODE_ID_PROPERTY: + Long oldId = Long.valueOf(value); + _nodeStack.peek().put(_OLD_ENTRY_ID_PROPERTY, oldId); + // Start forwarding characters events again + _flags |= FORWARD_CHARACTERS; + break; + default: + break; + } + } + + /** + * Retrieves the currentEntryId. + * @return the currentEntryId + */ + public long getCurrentEntryId() + { + return _currentEntryId; + } + + /** + * Retrieves the idMapping. + * @return the idMapping + */ + public Map getIdMapping() + { + return _idMapping; + } +} Index: main/plugin-cms/src/org/ametys/cms/io/handlers/ExportResourcesHandler.java =================================================================== --- main/plugin-cms/src/org/ametys/cms/io/handlers/ExportResourcesHandler.java (revision 0) +++ main/plugin-cms/src/org/ametys/cms/io/handlers/ExportResourcesHandler.java (revision 0) @@ -0,0 +1,185 @@ +/* + * Copyright 2012 Anyware Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ametys.cms.io.handlers; + +import java.util.HashSet; +import java.util.Properties; +import java.util.Set; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.apache.commons.lang.StringUtils; +import org.apache.jackrabbit.JcrConstants; +import org.xml.sax.Attributes; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; + +import org.ametys.cms.io.IOConstants; +import org.ametys.cms.io.IOHandler; +import org.ametys.plugins.explorer.resources.jcr.JCRResourceFactory; +import org.ametys.plugins.explorer.resources.jcr.JCRResourcesCollectionFactory; + +/** + * "Proxy" handler used to collects useful information while exporting the resources of a site. + */ +public class ExportResourcesHandler extends IOHandler +{ + /** primary type property name */ + protected static final String _PRIMARY_TYPE_PROPERTY_NAME = "primaryType"; + + /** Set tracking resources that should be imported */ + private final Set _resources = new HashSet(); + + /** + * Ctor inheritance + * @param contentHandler The {@link ContentHandler} to be wrapped. + * @param parentNode The node from which the import/export is relative + */ + public ExportResourcesHandler(final ContentHandler contentHandler, Node parentNode) + { + super(contentHandler, parentNode); + } + + @Override + protected void _onNodeStart(Attributes atts) + { + // nothing to do. + } + + @Override + protected void _onPropertyStart(Attributes atts) + { + String name = atts.getValue(IOConstants.SV_NAME); + + if (JcrConstants.JCR_PRIMARYTYPE.equals(name)) + { + _state = IN_JCR_PRIMARY_TYPE_PROPERTY; + } + } + + @Override + protected void _onValueStart(Attributes atts) + { + switch (_state) + { + case IN_JCR_PRIMARY_TYPE_PROPERTY: + // Set the scan characters flag + _flags |= SCAN_CHARACTERS; + break; + default: + break; + } + } + + @Override + protected void _onNodeEnd() throws SAXException + { + // Retrieves the node properties. + Properties props = _nodeStack.peek(); + + // Retrieves primary type + String primaryType = (String) props.get(_PRIMARY_TYPE_PROPERTY_NAME); + if (primaryType == null) + { + return; + } + + // Populate the resource map + boolean isResourceFile = false; + boolean isResourceDir = false; + if (JCRResourceFactory.RESOURCES_NODETYPE.equals(primaryType)) + { + isResourceFile = true; + + } + else if (JCRResourcesCollectionFactory.RESOURCESCOLLECTION_NODETYPE.equals(primaryType)) + { + isResourceDir = true; + } + + if (isResourceFile || isResourceDir) + { + String excludedBasePath = null; + try + { + excludedBasePath = _parentNode.getName() + '/'; + } + catch (RepositoryException e) + { + String msg = "Error during the export of the resource '" + props.get(NAME_PROPERTY_NAME) + "' node."; + _logger.error(msg, e); + throw new SAXException(msg, e); + } + + // Remove node that path are contained in the excluded base path. + String path = (String) props.get(PATH_PROPERTY_NAME); + if (excludedBasePath.startsWith(path)) + { + return; + } + + // Remove excluded base path from the path + path = StringUtils.removeStart(path, excludedBasePath); + + // Suffix with '/' if is directory + if (isResourceDir) + { + path += '/'; + } + + _resources.add(path); + } + } + + @Override + protected void _onPropertyEnd() + { + switch (_state) + { + case IN_JCR_PRIMARY_TYPE_PROPERTY: + _state = IN_NODE; + break; + default: + break; + } + } + + @Override + protected void _onValueEnd(String value) + { + switch (_state) + { + case IN_JCR_PRIMARY_TYPE_PROPERTY: + if (value != null) + { + _nodeStack.peek().put(_PRIMARY_TYPE_PROPERTY_NAME, value); + } + break; + default: + break; + } + } + + /** + * Retrieves the resources. + * @return the resources + */ + public Set getResources() + { + return _resources; + } +} Index: main/plugin-cms/src/org/ametys/cms/io/IOConstants.java =================================================================== --- main/plugin-cms/src/org/ametys/cms/io/IOConstants.java (revision 0) +++ main/plugin-cms/src/org/ametys/cms/io/IOConstants.java (revision 0) @@ -0,0 +1,98 @@ +/* + * Copyright 2012 Anyware Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ametys.cms.io; + +import org.ametys.plugins.repository.RepositoryConstants; + +/** + * Collection of constants used in the import/export + */ +public final class IOConstants +{ + // System view elements + /** system view prefix */ + public static final String SV_PREFIX = "sv:"; + /** sv:node node */ + public static final String SV_NODE = SV_PREFIX + "node"; + /** sv:property node */ + public static final String SV_PROPERTY = SV_PREFIX + "property"; + /** sv:value node */ + public static final String SV_VALUE = SV_PREFIX + "value"; + /** sv:name attribute */ + public static final String SV_NAME = SV_PREFIX + "name"; + /** sv:type attribute */ + public static final String SV_TYPE = SV_PREFIX + "type"; + + /** The Ametys internal workflow reference property name */ + public static final String WORKFLOW_REF_PROPERTY_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":workflowRef"; + /** The Ametys internal content reference property name */ + public static final String CONTENT_REF_PROPERTY_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":contentRef"; + /** The Ametys internal workflow id property name */ + public static final String WORKFLOW_ID_PROPERTY = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":workflowId"; + /** The Ametys internal current step id property name */ +// public static final String CURRENT_STEP_ID_PROPERTY = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":currentStepId"; + /** Constant for resources node name. */ + public static final String RESOURCES_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":resources"; + /** Constant for contents node name. */ + public static final String CONTENTS_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":contents"; + /** The Ametys default content nodeType */ + public static final String DEFAULT_CONTENT_NODETYPE = RepositoryConstants.NAMESPACE_PREFIX + ":defaultContent"; + + /** oswf prefix */ + public static final String OSWF_PREFIX = "oswf:"; + /** oswf entry node name prefix. */ + public static final String OSWF_ROOT = OSWF_PREFIX + "root"; + /** oswf entry node name prefix. */ + public static final String OSWF_ROOT_NT = OSWF_PREFIX + "root"; + /** oswf entry node type. */ + public static final String OSWF_ENTRY_NT = OSWF_PREFIX + "entry"; + /** Internal hash tree node type. */ + public static final String OSWF_HTREE_NT = OSWF_PREFIX + "hashSegment"; + /** ID property for oswf entries */ + public static final String ENTRY_NODE_ID_PROPERTY = OSWF_PREFIX + "id"; + /** Next entry id property for root oswf node. */ + public static final String NEXT_ENTRY_ID_PROPERTY = OSWF_PREFIX + "nextEntryId"; + /** oswf:currentStep node name */ + public static final String OSWF_CURRENT_STEP = OSWF_PREFIX + "currentStep"; + /** Entry node name prefix. */ + public static final String ENTRY_NODE_PREFIX = "workflow-"; + + /** System view extension */ + public static final String SYSTEM_VIEW_EXT = ".sv"; + /** Archive workflows filename */ + public static final String WORKFLOWS_FILE_NAME = "workflows" + SYSTEM_VIEW_EXT; + /** Archives resources filename */ + public static final String RESOURCES_FILE_NAME = "resources" + SYSTEM_VIEW_EXT; + + /** Archive data folder */ + public static final String BINARY_DIR = "data/"; + /** Archive archives data folder */ + public static final String ARCHIVES_DIR = "archives/"; + /** Archive explorer data folder */ + public static final String EXPLORER_DIR = "explorer/"; + /** Archive resources data folder */ + public static final String RESOURCES_DIR = "_resources/"; + + /** Name of the root element in the workflow exported file */ + public static final String WORKFLOW_FILE_ROOT_NODE = "oswfroot"; + + /** + * Private constructor to prevent instantiation of this class. + */ + private IOConstants() + { + } +} Index: main/plugin-cms/src/org/ametys/cms/io/IOHandler.java =================================================================== --- main/plugin-cms/src/org/ametys/cms/io/IOHandler.java (revision 0) +++ main/plugin-cms/src/org/ametys/cms/io/IOHandler.java (revision 0) @@ -0,0 +1,371 @@ +/* + * Copyright 2012 Anyware Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ametys.cms.io; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.List; +import java.util.Properties; + +import javax.jcr.Node; + +import org.apache.avalon.framework.logger.Logger; +import org.apache.commons.lang.StringUtils; +import org.apache.excalibur.xml.sax.ContentHandlerProxy; +import org.xml.sax.Attributes; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; + +import org.ametys.cms.io.IOConstants; +import org.ametys.runtime.util.LoggerFactory; + +/** + * Abstract base class for every handler used in the import/export + */ +public abstract class IOHandler extends ContentHandlerProxy +{ + // State management + /** In node state */ + protected static final int IN_NODE = 0; + + // Properties matched for a specific processing + /** JCR primary type property */ + protected static final int IN_JCR_PRIMARY_TYPE_PROPERTY = 10; + /** JCR UUID property */ + protected static final int IN_JCR_UUID_PROPERTY = 11; + + // Set of handler flags used to control the behavior of the handler */ + /** SAX start events are forwarded when set */ + protected static final int FORWARD_STARTS = 1 << 0; + + /** SAX end events are forwarded when set */ + protected static final int FORWARD_ENDS = 1 << 1; + + /** Received characters data are forwarded when true */ + protected static final int FORWARD_CHARACTERS = 1 << 2; + + /** received characters data are scanned when set */ + protected static final int SCAN_CHARACTERS = 1 << 3; + + /** The very next SAX start event will not be are forwarded when set */ + protected static final int DO_NOT_FORWARD_NEXT_START = 1 << 4; + + /** Default flag options */ + protected static final int DEFAULT_FLAGS = FORWARD_STARTS | FORWARD_ENDS | FORWARD_CHARACTERS; + + /** path property name */ + protected static final String PATH_PROPERTY_NAME = "path"; + + /** name property name */ + protected static final String NAME_PROPERTY_NAME = "name"; + + /** children property name (private) */ + private static final String __CHILDREN_PROPERTY_NAME = "children"; + + /** avalon logger */ + protected final Logger _logger = LoggerFactory.getLoggerFor(IOHandler.class); + + /** Flag holder */ + protected int _flags = DEFAULT_FLAGS; + + /** The current state */ + protected int _state = IN_NODE; + + /** Deque that represent the current path relative to the base import node */ + protected final Deque _path = new ArrayDeque(); + + /** + * Stack of the currently processed nodes. For each node, some properties + * are collected and stocked into this stack. + */ + protected final Deque _nodeStack = new ArrayDeque(); + + /** The node from which the import/export is relative */ + protected Node _parentNode; + + /** An internal {@link StringBuilder} used to scan property values when needed */ + protected StringBuilder _sb; + + /** + * Ctor inheritance + * @param contentHandler The {@link ContentHandler} to be wrapped. + * @param parentNode The node from which the import/export is relative + */ + public IOHandler(final ContentHandler contentHandler, Node parentNode) + { + super(contentHandler); + _parentNode = parentNode; + } + + @Override + public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException + { + // In 'node' node, keep tracking of current path. + if (IOConstants.SV_NODE.equals(qName)) + { + _analyzeNodeOnStart(atts); + _onNodeStart(atts); + } + // In property node, analyze attributes of interest. + else if (IOConstants.SV_PROPERTY.equals(qName)) + { + _onPropertyStart(atts); + } + // In value node, retrieves value of interest. + else if (IOConstants.SV_VALUE.equals(qName)) + { + _onValueStart(atts); + if ((_flags & SCAN_CHARACTERS) == SCAN_CHARACTERS) + { + _sb = new StringBuilder(); + } + } + + if ((_flags & DO_NOT_FORWARD_NEXT_START) == DO_NOT_FORWARD_NEXT_START) + { + _flags &= ~DO_NOT_FORWARD_NEXT_START; + } + else if ((_flags & FORWARD_STARTS) == FORWARD_STARTS) + { + super.startElement(namespaceURI, localName, qName, atts); + } + } + + /** + * Notify the start of a sv:node node. + * Analyze the sv:node on the start event, and initialize its properties. + * @param atts + * @throws SAXException + * @see ContentHandler#startElement(String, String, String, Attributes) + */ + protected void _analyzeNodeOnStart(Attributes atts) throws SAXException + { + String name = atts.getValue(IOConstants.SV_NAME); + if (StringUtils.isNotEmpty(name)) + { + // Reference this node as a new child + _addNewChildToCurrentNode(name); + + // Update current _indexed_ path (which means, dealing with same-name siblings issue) + int count = _countCurrentChildrenWithName(name); + String indexedName = count <= 1 ? name : name + '[' + count + ']'; + _path.push(indexedName); + + // New properties for this node, push them into the stack. + Properties props = new Properties(); + props.put(PATH_PROPERTY_NAME, _getCurrentPath()); + props.put(NAME_PROPERTY_NAME, name); + _nodeStack.push(props); + } + else + { + String msg = "Attribute '" + IOConstants.SV_NAME + "' was expected but not found."; + _logger.error(msg); + throw new SAXException(msg); + } + } + + /** + * Notify the start of a sv:node node. + * @param atts + * @throws SAXException + * @see ContentHandler#startElement(String, String, String, Attributes) + */ + protected abstract void _onNodeStart(Attributes atts) throws SAXException; + + /** + * Notify the start of a sv:property node. + * @param atts + * @throws SAXException + * @see ContentHandler#startElement(String, String, String, Attributes) + */ + protected abstract void _onPropertyStart(Attributes atts) throws SAXException; + + /** + * Notify the start of a sv:value node. + * @param atts + * @throws SAXException + * @see ContentHandler#startElement(String, String, String, Attributes) + */ + protected abstract void _onValueStart(Attributes atts) throws SAXException; + + @Override + public void characters (char[] ch, int start, int length) throws SAXException + { + if ((_flags & FORWARD_CHARACTERS) == FORWARD_CHARACTERS) + { + super.characters(ch, start, length); + } + + if ((_flags & SCAN_CHARACTERS) == SCAN_CHARACTERS) + { + _sb.append(ch, start, length); + } + } + + @Override + public void endElement(String namespaceURI, String localName, String qName) throws SAXException + { + if ((_flags & FORWARD_ENDS) == FORWARD_ENDS) + { + super.endElement(namespaceURI, localName, qName); + } + + if (IOConstants.SV_NODE.equals(qName)) + { + _onNodeEnd(); + _path.pop(); + _nodeStack.pop(); + } + else if (IOConstants.SV_PROPERTY.equals(qName)) + { + _onPropertyEnd(); + } + // In value node, retrieves value of interest. + else if (IOConstants.SV_VALUE.equals(qName)) + { + String value = null; + if ((_flags & SCAN_CHARACTERS) == SCAN_CHARACTERS) + { + if (_sb.length() > 0) + { + value = _sb.toString(); + _sb.setLength(0); + } + + // Remove scan characters flag. + _flags &= ~SCAN_CHARACTERS; + } + _onValueEnd(value); + } + } + + /** + * Notify the end of a sv:node node. + * @throws SAXException + * @see ContentHandler#endElement(String, String, String) + */ + protected abstract void _onNodeEnd() throws SAXException; + + /** + * Notify the end of a sv:property node. + * @throws SAXException + * @see ContentHandler#endElement(String, String, String) + */ + protected abstract void _onPropertyEnd() throws SAXException; + + /** + * Notify the end of a sv:value node. + * @param value The scanned value. null if scan flag is not set or if the + * retrieved value if empty. + * @throws SAXException + * @see ContentHandler#endElement(String, String, String) + */ + protected abstract void _onValueEnd(String value) throws SAXException; + + /** + * Manually send a start element notification. + * @param namespaceURI + * @param localName + * @param qName + * @param atts + * @throws SAXException + */ + protected void sendStartElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException + { + super.startElement(namespaceURI, localName, qName, atts); + } + + /** + * Manually send a characters notification. + * @param ch + * @param start + * @param length + * @throws SAXException + */ + public void sendCharacters (char[] ch, int start, int length) throws SAXException + { + super.characters(ch, start, length); + } + + /** + * Manually send an end element notification. + * @param namespaceURI + * @param localName + * @param qName + * @throws SAXException + */ + protected void sendEndElement(String namespaceURI, String localName, String qName) throws SAXException + { + super.endElement(namespaceURI, localName, qName); + } + + /** + * Retrieves the path of the currently imported node. + * @return The retrieved path. + */ + protected String _getCurrentPath() + { + return StringUtils.join(_path.descendingIterator(), '/'); + } + + /** + * Retrieves the currently imported/exported child nodes of the current node. + * @return the list of the children. + */ + protected List _getCurrentNodeChildren() + { + if (_nodeStack.isEmpty()) + { + return new ArrayList(); + } + + List children = (List) _nodeStack.peek().get(__CHILDREN_PROPERTY_NAME); + if (children == null) + { + return new ArrayList(); + } + + return children; + } + + private void _addNewChildToCurrentNode(String name) + { + if (_nodeStack.isEmpty()) // if empty, we are importing the root node. + { + return; + } + + List children = (List) _nodeStack.peek().get(__CHILDREN_PROPERTY_NAME); + if (children == null) + { + children = new ArrayList(); + children.add(name); + _nodeStack.peek().put(__CHILDREN_PROPERTY_NAME, children); + } + else + { + children.add(name); + } + } + + private int _countCurrentChildrenWithName(String name) + { + return Collections.frequency(_getCurrentNodeChildren(), name); + } +} Index: main/plugin-cms/src/org/ametys/cms/workflow/ValidationStepFunction.java =================================================================== --- main/plugin-cms/src/org/ametys/cms/workflow/ValidationStepFunction.java (revision 19862) +++ main/plugin-cms/src/org/ametys/cms/workflow/ValidationStepFunction.java (working copy) @@ -15,7 +15,6 @@ */ package org.ametys.cms.workflow; -import java.util.List; import java.util.Map; import org.ametys.plugins.workflow.store.AmetysStep; @@ -23,6 +22,7 @@ import com.opensymphony.module.propertyset.PropertySet; import com.opensymphony.workflow.FunctionProvider; import com.opensymphony.workflow.WorkflowException; +import com.opensymphony.workflow.spi.Step; /** * OSWorkflow function for setting the "validate" flag on steps, indicating they represent a validation step.
@@ -33,11 +33,11 @@ @Override public void execute(Map transientVars, Map args, PropertySet ps) throws WorkflowException { - List steps = (List) transientVars.get("currentSteps"); + Step createdStep = (Step) transientVars.get("createdStep"); - for (AmetysStep step : steps) + if (createdStep != null && createdStep instanceof AmetysStep) { - step.setProperty("validation", true); + ((AmetysStep) createdStep).setProperty("validation", true); } } }