NXP-3638: add BulkEditActions Seam bean to handle bulk edit on a set of Documents 5.3
authorThomas Roger <troger@nuxeo.com>
Fri Jul 02 15:17:15 2010 +0200 (22 months ago)
branch5.3
changeset 4114a222a069d1c
parent 410 4ec8ffe34ce5
child 412 dae0222dae06
child 414 6ced57fa33f1
child 416 7c3528c03e51
child 418 8db331b449fb
child 454 ac420c5bc2ed
child 455 f9261be1ae11
child 464 a2be2fafb0da
NXP-3638: add BulkEditActions Seam bean to handle bulk edit on a set of Documents
nuxeo-platform-webapp-base/pom.xml
nuxeo-platform-webapp-base/src/main/java/org/nuxeo/ecm/webapp/bulkedit/BulkEditActions.java
nuxeo-platform-webapp-base/src/main/java/org/nuxeo/ecm/webapp/bulkedit/BulkEditHelper.java
nuxeo-platform-webapp-base/src/main/java/org/nuxeo/ecm/webapp/bulkedit/FictiveDataModel.java
nuxeo-platform-webapp-base/src/main/java/org/nuxeo/ecm/webapp/bulkedit/FictiveDocumentModel.java
nuxeo-platform-webapp-base/src/test/java/org/nuxeo/ecm/webapp/bulkedit/TestBulkEditHelper.java
nuxeo-platform-webapp-base/src/test/resources/log4j.properties
pom.xml
     1.1 --- a/nuxeo-platform-webapp-base/pom.xml
     1.2 +++ b/nuxeo-platform-webapp-base/pom.xml
     1.3 @@ -263,6 +263,16 @@
     1.4        <artifactId>nuxeo-core-jcr-connector-test</artifactId>
     1.5        <scope>test</scope>
     1.6      </dependency>
     1.7 +    <dependency>
     1.8 +      <groupId>org.nuxeo.ecm.core</groupId>
     1.9 +      <artifactId>nuxeo-core-test</artifactId>
    1.10 +      <scope>test</scope>
    1.11 +    </dependency>
    1.12 +    <dependency>
    1.13 +      <groupId>org.nuxeo.ecm.platform</groupId>
    1.14 +      <artifactId>nuxeo-platform-types-core</artifactId>
    1.15 +      <scope>test</scope>
    1.16 +    </dependency>
    1.17    </dependencies>
    1.18  
    1.19    <build>
     2.1 new file mode 100644
     2.2 --- /dev/null
     2.3 +++ b/nuxeo-platform-webapp-base/src/main/java/org/nuxeo/ecm/webapp/bulkedit/BulkEditActions.java
     2.4 @@ -0,0 +1,116 @@
     2.5 +/*
     2.6 + * (C) Copyright 2009 Nuxeo SA (http://nuxeo.com/) and contributors.
     2.7 + *
     2.8 + * All rights reserved. This program and the accompanying materials
     2.9 + * are made available under the terms of the GNU Lesser General Public License
    2.10 + * (LGPL) version 2.1 which accompanies this distribution, and is available at
    2.11 + * http://www.gnu.org/licenses/lgpl.html
    2.12 + *
    2.13 + * This library is distributed in the hope that it will be useful,
    2.14 + * but WITHOUT ANY WARRANTY; without even the implied warranty of
    2.15 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
    2.16 + * Lesser General Public License for more details.
    2.17 + *
    2.18 + * Contributors:
    2.19 + *     Thomas Roger
    2.20 + */
    2.21 +
    2.22 +package org.nuxeo.ecm.webapp.bulkedit;
    2.23 +
    2.24 +import java.io.Serializable;
    2.25 +import java.util.Collections;
    2.26 +import java.util.List;
    2.27 +
    2.28 +import org.jboss.seam.ScopeType;
    2.29 +import org.jboss.seam.annotations.Factory;
    2.30 +import org.jboss.seam.annotations.In;
    2.31 +import org.jboss.seam.annotations.Install;
    2.32 +import org.jboss.seam.annotations.Name;
    2.33 +import org.jboss.seam.annotations.Scope;
    2.34 +import org.nuxeo.ecm.core.api.ClientException;
    2.35 +import org.nuxeo.ecm.core.api.CoreSession;
    2.36 +import org.nuxeo.ecm.core.api.DocumentModel;
    2.37 +import org.nuxeo.ecm.platform.types.TypeManager;
    2.38 +import org.nuxeo.ecm.platform.ui.web.api.NavigationContext;
    2.39 +import org.nuxeo.ecm.webapp.documentsLists.DocumentsListsManager;
    2.40 +
    2.41 +import static org.jboss.seam.ScopeType.CONVERSATION;
    2.42 +
    2.43 +/**
    2.44 + * Handles Bulk Edit actions.
    2.45 + *
    2.46 + * @author <a href="mailto:troger@nuxeo.com">Thomas Roger</a>
    2.47 + */
    2.48 +@Name("bulkEditActions")
    2.49 +@Scope(CONVERSATION)
    2.50 +@Install(precedence = Install.FRAMEWORK)
    2.51 +public class BulkEditActions implements Serializable {
    2.52 +
    2.53 +    private static final long serialVersionUID = 1L;
    2.54 +
    2.55 +    @In(create = true)
    2.56 +    protected transient DocumentsListsManager documentsListsManager;
    2.57 +
    2.58 +    @In(create = true)
    2.59 +    protected transient TypeManager typeManager;
    2.60 +
    2.61 +    @In(create = true)
    2.62 +    protected transient CoreSession documentManager;
    2.63 +
    2.64 +    @In(create = true)
    2.65 +    protected transient NavigationContext navigationContext;
    2.66 +
    2.67 +    protected DocumentModel fictiveDocumentModel;
    2.68 +
    2.69 +    /**
    2.70 +     * Returns the common layouts of the current selected documents for the
    2.71 +     * {@code edit} mode.
    2.72 +     */
    2.73 +    public List<String> getCommonsLayouts() {
    2.74 +        if (documentsListsManager.isWorkingListEmpty(DocumentsListsManager.CURRENT_DOCUMENT_SELECTION)) {
    2.75 +            return Collections.emptyList();
    2.76 +        }
    2.77 +
    2.78 +        List<DocumentModel> selectedDocuments = documentsListsManager.getWorkingList(DocumentsListsManager.CURRENT_DOCUMENT_SELECTION);
    2.79 +        return BulkEditHelper.getCommonLayouts(typeManager, selectedDocuments);
    2.80 +    }
    2.81 +
    2.82 +    /**
    2.83 +     * Returns the common schemas for the current selected documents.
    2.84 +     */
    2.85 +    protected List<String> getCommonSchemas() {
    2.86 +        if (documentsListsManager.isWorkingListEmpty(DocumentsListsManager.CURRENT_DOCUMENT_SELECTION)) {
    2.87 +            return Collections.emptyList();
    2.88 +        }
    2.89 +
    2.90 +        List<DocumentModel> selectedDocuments = documentsListsManager.getWorkingList(DocumentsListsManager.CURRENT_DOCUMENT_SELECTION);
    2.91 +        return BulkEditHelper.getCommonSchemas(selectedDocuments);
    2.92 +    }
    2.93 +
    2.94 +    @Factory(value = "bulkEditDocumentModel", scope = ScopeType.EVENT)
    2.95 +    public DocumentModel getBulkEditDocumentModel() {
    2.96 +        if (fictiveDocumentModel == null) {
    2.97 +            fictiveDocumentModel = FictiveDocumentModel.createFictiveDocumentModelWith(getCommonSchemas());
    2.98 +        }
    2.99 +        return fictiveDocumentModel;
   2.100 +    }
   2.101 +
   2.102 +    public String bulkEditSelection() throws ClientException {
   2.103 +        bulkEditSelectionNoRedirect();
   2.104 +        return navigationContext.navigateToDocument(navigationContext.getCurrentDocument());
   2.105 +    }
   2.106 +
   2.107 +    public void bulkEditSelectionNoRedirect() throws ClientException {
   2.108 +        if (fictiveDocumentModel != null) {
   2.109 +            List<DocumentModel> selectedDocuments = documentsListsManager.getWorkingList(DocumentsListsManager.CURRENT_DOCUMENT_SELECTION);
   2.110 +            BulkEditHelper.copyMetadata(documentManager, fictiveDocumentModel,
   2.111 +                    selectedDocuments);
   2.112 +            fictiveDocumentModel = null;
   2.113 +        }
   2.114 +    }
   2.115 +
   2.116 +    public void cancel() {
   2.117 +        fictiveDocumentModel = null;
   2.118 +    }
   2.119 +
   2.120 +}
     3.1 new file mode 100644
     3.2 --- /dev/null
     3.3 +++ b/nuxeo-platform-webapp-base/src/main/java/org/nuxeo/ecm/webapp/bulkedit/BulkEditHelper.java
     3.4 @@ -0,0 +1,136 @@
     3.5 +/*
     3.6 + * (C) Copyright 2009 Nuxeo SA (http://nuxeo.com/) and contributors.
     3.7 + *
     3.8 + * All rights reserved. This program and the accompanying materials
     3.9 + * are made available under the terms of the GNU Lesser General Public License
    3.10 + * (LGPL) version 2.1 which accompanies this distribution, and is available at
    3.11 + * http://www.gnu.org/licenses/lgpl.html
    3.12 + *
    3.13 + * This library is distributed in the hope that it will be useful,
    3.14 + * but WITHOUT ANY WARRANTY; without even the implied warranty of
    3.15 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
    3.16 + * Lesser General Public License for more details.
    3.17 + *
    3.18 + * Contributors:
    3.19 + *     Thomas Roger
    3.20 + */
    3.21 +
    3.22 +package org.nuxeo.ecm.webapp.bulkedit;
    3.23 +
    3.24 +import java.io.Serializable;
    3.25 +import java.util.ArrayList;
    3.26 +import java.util.Arrays;
    3.27 +import java.util.List;
    3.28 +import java.util.Map;
    3.29 +
    3.30 +import org.nuxeo.ecm.core.api.ClientException;
    3.31 +import org.nuxeo.ecm.core.api.CoreSession;
    3.32 +import org.nuxeo.ecm.core.api.DocumentModel;
    3.33 +import org.nuxeo.ecm.platform.forms.layout.api.BuiltinModes;
    3.34 +import org.nuxeo.ecm.platform.types.Type;
    3.35 +import org.nuxeo.ecm.platform.types.TypeManager;
    3.36 +
    3.37 +/**
    3.38 + * Helper used for bulk edit actions
    3.39 + *
    3.40 + * @author <a href="mailto:troger@nuxeo.com">Thomas Roger</a>
    3.41 + */
    3.42 +public class BulkEditHelper {
    3.43 +
    3.44 +    public static final String BULK_EDIT_PREFIX = "bulkEdit/";
    3.45 +
    3.46 +    private BulkEditHelper() {
    3.47 +        // Helper class
    3.48 +    }
    3.49 +
    3.50 +    /**
    3.51 +     * Returns the common layouts of the {@code docs} for the {@code edit} mode.
    3.52 +     */
    3.53 +    public static List<String> getCommonLayouts(TypeManager typeManager,
    3.54 +            List<DocumentModel> docs) {
    3.55 +        return getCommonLayouts(typeManager, docs, BuiltinModes.EDIT);
    3.56 +    }
    3.57 +
    3.58 +    /**
    3.59 +     * Returns the common layouts of the {@code docs} for the given layout
    3.60 +     * {@code mode}.
    3.61 +     */
    3.62 +    public static List<String> getCommonLayouts(TypeManager typeManager,
    3.63 +            List<DocumentModel> docs, String mode) {
    3.64 +        List<String> layouts = null;
    3.65 +        for (DocumentModel doc : docs) {
    3.66 +            Type type = typeManager.getType(doc.getType());
    3.67 +            List<String> typeLayouts = Arrays.asList(type.getLayouts(mode));
    3.68 +            if (layouts == null) {
    3.69 +                // first document
    3.70 +                layouts = new ArrayList<String>();
    3.71 +                layouts.addAll(typeLayouts);
    3.72 +            } else {
    3.73 +                layouts.retainAll(typeLayouts);
    3.74 +            }
    3.75 +        }
    3.76 +        return layouts;
    3.77 +    }
    3.78 +
    3.79 +    /**
    3.80 +     * Returns the common schemas of the {@code docs}.
    3.81 +     */
    3.82 +    public static List<String> getCommonSchemas(List<DocumentModel> docs) {
    3.83 +        List<String> schemas = null;
    3.84 +        for (DocumentModel doc : docs) {
    3.85 +            List<String> docSchemas = Arrays.asList(doc.getDeclaredSchemas());
    3.86 +            if (schemas == null) {
    3.87 +                // first document
    3.88 +                schemas = new ArrayList<String>();
    3.89 +                schemas.addAll(docSchemas);
    3.90 +            } else {
    3.91 +                schemas.retainAll(docSchemas);
    3.92 +            }
    3.93 +        }
    3.94 +        return schemas;
    3.95 +    }
    3.96 +
    3.97 +    /**
    3.98 +     * Copy all the marked properties (stored in the ContextData of {@code
    3.99 +     * sourceDoc}) from {@code sourceDoc} to all the {@code targetDocs}.
   3.100 +     *
   3.101 +     * @param session the {@code CoreSession} to use
   3.102 +     * @param sourceDoc the doc where to get the metadata to copy
   3.103 +     * @param targetDocs the docs where to set the metadatas
   3.104 +     */
   3.105 +    public static void copyMetadata(CoreSession session,
   3.106 +            DocumentModel sourceDoc, List<DocumentModel> targetDocs)
   3.107 +            throws ClientException {
   3.108 +        List<String> propertiesToCopy = getPropertiesToCopy(sourceDoc);
   3.109 +        for (DocumentModel targetDoc : targetDocs) {
   3.110 +            for (String propertyToCopy : propertiesToCopy) {
   3.111 +                targetDoc.setPropertyValue(propertyToCopy,
   3.112 +                        sourceDoc.getPropertyValue(propertyToCopy));
   3.113 +            }
   3.114 +        }
   3.115 +        session.saveDocuments(targetDocs.toArray(new DocumentModel[targetDocs.size()]));
   3.116 +        session.save();
   3.117 +    }
   3.118 +
   3.119 +    /**
   3.120 +     * Extracts the properties to be copied from {@code sourceDoc}. The
   3.121 +     * properties are stored in the ContextData of {@code sourceDoc}: the key
   3.122 +     * is the xpath property, the value is {@code true} if the property has to
   3.123 +     * be copied, {@code false otherwise}.
   3.124 +     */
   3.125 +    protected static List<String> getPropertiesToCopy(DocumentModel sourceDoc) {
   3.126 +        List<String> propertiesToCopy = new ArrayList<String>();
   3.127 +        for (Map.Entry<String, Serializable> entry : sourceDoc.getContextData().entrySet()) {
   3.128 +            String key = entry.getKey();
   3.129 +            if (key.startsWith(BULK_EDIT_PREFIX)) {
   3.130 +                String[] properties = key.replace(BULK_EDIT_PREFIX, "").split(" ");
   3.131 +                Serializable value = entry.getValue();
   3.132 +                if (value instanceof Boolean && (Boolean) value) {
   3.133 +                    propertiesToCopy.addAll(Arrays.asList(properties));
   3.134 +                }
   3.135 +            }
   3.136 +        }
   3.137 +        return propertiesToCopy;
   3.138 +    }
   3.139 +
   3.140 +}
     4.1 new file mode 100644
     4.2 --- /dev/null
     4.3 +++ b/nuxeo-platform-webapp-base/src/main/java/org/nuxeo/ecm/webapp/bulkedit/FictiveDataModel.java
     4.4 @@ -0,0 +1,93 @@
     4.5 +/*
     4.6 + * (C) Copyright 2009 Nuxeo SA (http://nuxeo.com/) and contributors.
     4.7 + *
     4.8 + * All rights reserved. This program and the accompanying materials
     4.9 + * are made available under the terms of the GNU Lesser General Public License
    4.10 + * (LGPL) version 2.1 which accompanies this distribution, and is available at
    4.11 + * http://www.gnu.org/licenses/lgpl.html
    4.12 + *
    4.13 + * This library is distributed in the hope that it will be useful,
    4.14 + * but WITHOUT ANY WARRANTY; without even the implied warranty of
    4.15 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
    4.16 + * Lesser General Public License for more details.
    4.17 + *
    4.18 + * Contributors:
    4.19 + *     Thomas Roger
    4.20 + */
    4.21 +
    4.22 +package org.nuxeo.ecm.webapp.bulkedit;
    4.23 +
    4.24 +import java.util.Collection;
    4.25 +import java.util.HashMap;
    4.26 +import java.util.Map;
    4.27 +
    4.28 +import org.nuxeo.ecm.core.api.DataModel;
    4.29 +import org.nuxeo.ecm.core.api.model.DocumentPart;
    4.30 +import org.nuxeo.ecm.core.api.model.PropertyException;
    4.31 +import org.nuxeo.ecm.core.api.model.PropertyNotFoundException;
    4.32 +import org.nuxeo.ecm.core.api.model.impl.DefaultPropertyFactory;
    4.33 +
    4.34 +/**
    4.35 + * A data model that is not tied to a particular schema, neither has anything to
    4.36 + * do with a session (CoreSession).
    4.37 + * <p>
    4.38 + * Used just to hold data for a <code>FictiveDocumentModel</code> in a
    4.39 + * non-constraining way.
    4.40 + *
    4.41 + * @author DM
    4.42 + * @author <a href="mailto:troger@nuxeo.com">Thomas Roger</a>
    4.43 + */
    4.44 +public class FictiveDataModel implements DataModel {
    4.45 +
    4.46 +    protected Map<String, Object> data = new HashMap<String, Object>();
    4.47 +
    4.48 +    protected final DocumentPart dp;
    4.49 +
    4.50 +    public FictiveDataModel(String schema) {
    4.51 +        dp = DefaultPropertyFactory.newDocumentPart(schema);
    4.52 +    }
    4.53 +
    4.54 +    public void setData(String key, Object value) throws PropertyException {
    4.55 +        data.put(key, value);
    4.56 +    }
    4.57 +
    4.58 +    public Object getData(String key) throws PropertyException {
    4.59 +        return data.get(key);
    4.60 +    }
    4.61 +
    4.62 +    public String getSchema() {
    4.63 +        return dp.getSchema().getSchemaName();
    4.64 +    }
    4.65 +
    4.66 +    public Map<String, Object> getMap() throws PropertyException {
    4.67 +        return data;
    4.68 +    }
    4.69 +
    4.70 +    public void setMap(Map<String, Object> data) throws PropertyException {
    4.71 +        this.data = new HashMap<String, Object>(data);
    4.72 +    }
    4.73 +
    4.74 +    public boolean isDirty() {
    4.75 +        return true;
    4.76 +    }
    4.77 +
    4.78 +    public boolean isDirty(String name) throws PropertyNotFoundException {
    4.79 +        return true;
    4.80 +    }
    4.81 +
    4.82 +    public void setDirty(String name) throws PropertyNotFoundException {
    4.83 +    }
    4.84 +
    4.85 +    public Collection<String> getDirtyFields() {
    4.86 +        return data.keySet();
    4.87 +    }
    4.88 +
    4.89 +    public Object getValue(String path) throws PropertyException {
    4.90 +        throw new UnsupportedOperationException();
    4.91 +    }
    4.92 +
    4.93 +    public Object setValue(String path, Object value) throws PropertyException {
    4.94 +        throw new UnsupportedOperationException();
    4.95 +    }
    4.96 +
    4.97 +}
     5.1 new file mode 100644
     5.2 --- /dev/null
     5.3 +++ b/nuxeo-platform-webapp-base/src/main/java/org/nuxeo/ecm/webapp/bulkedit/FictiveDocumentModel.java
     5.4 @@ -0,0 +1,416 @@
     5.5 +/*
     5.6 + * (C) Copyright 2009 Nuxeo SA (http://nuxeo.com/) and contributors.
     5.7 + *
     5.8 + * All rights reserved. This program and the accompanying materials
     5.9 + * are made available under the terms of the GNU Lesser General Public License
    5.10 + * (LGPL) version 2.1 which accompanies this distribution, and is available at
    5.11 + * http://www.gnu.org/licenses/lgpl.html
    5.12 + *
    5.13 + * This library is distributed in the hope that it will be useful,
    5.14 + * but WITHOUT ANY WARRANTY; without even the implied warranty of
    5.15 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
    5.16 + * Lesser General Public License for more details.
    5.17 + *
    5.18 + * Contributors:
    5.19 + *     Thomas Roger
    5.20 + */
    5.21 +
    5.22 +package org.nuxeo.ecm.webapp.bulkedit;
    5.23 +
    5.24 +import java.io.Serializable;
    5.25 +import java.util.Arrays;
    5.26 +import java.util.Collection;
    5.27 +import java.util.List;
    5.28 +import java.util.Map;
    5.29 +import java.util.Set;
    5.30 +
    5.31 +import org.nuxeo.common.collections.ScopeType;
    5.32 +import org.nuxeo.common.collections.ScopedMap;
    5.33 +import org.nuxeo.common.utils.Path;
    5.34 +import org.nuxeo.ecm.core.api.ClientException;
    5.35 +import org.nuxeo.ecm.core.api.CoreSession;
    5.36 +import org.nuxeo.ecm.core.api.DataModel;
    5.37 +import org.nuxeo.ecm.core.api.DataModelMap;
    5.38 +import org.nuxeo.ecm.core.api.DocumentException;
    5.39 +import org.nuxeo.ecm.core.api.DocumentModel;
    5.40 +import org.nuxeo.ecm.core.api.DocumentRef;
    5.41 +import org.nuxeo.ecm.core.api.impl.DataModelMapImpl;
    5.42 +import org.nuxeo.ecm.core.api.model.DocumentPart;
    5.43 +import org.nuxeo.ecm.core.api.model.Property;
    5.44 +import org.nuxeo.ecm.core.api.model.PropertyException;
    5.45 +import org.nuxeo.ecm.core.api.model.PropertyNotFoundException;
    5.46 +import org.nuxeo.ecm.core.api.model.impl.DefaultPropertyFactory;
    5.47 +import org.nuxeo.ecm.core.api.security.ACP;
    5.48 +import org.nuxeo.ecm.core.schema.DocumentType;
    5.49 +import org.nuxeo.ecm.core.schema.SchemaManager;
    5.50 +import org.nuxeo.ecm.core.schema.types.Schema;
    5.51 +import org.nuxeo.runtime.api.Framework;
    5.52 +
    5.53 +/**
    5.54 + * A DocumentModel that can have any schema and is not made persistent by
    5.55 + * itself. A mockup to keep arbitrary schema data.
    5.56 + *
    5.57 + * @author DM
    5.58 + * @author <a href="mailto:troger@nuxeo.com">Thomas Roger</a>
    5.59 + */
    5.60 +public class FictiveDocumentModel implements DocumentModel {
    5.61 +
    5.62 +    protected DataModelMap dataModels = new DataModelMapImpl();
    5.63 +
    5.64 +    protected ScopedMap contextData = new ScopedMap();
    5.65 +
    5.66 +    public static DocumentModel createFictiveDocumentModelWith(
    5.67 +            List<String> schemas) {
    5.68 +        FictiveDocumentModel doc = new FictiveDocumentModel();
    5.69 +        for (String schema : schemas) {
    5.70 +            DataModel dataModel = doc.dataModels.get(schema);
    5.71 +            if (dataModel == null) {
    5.72 +                dataModel = new FictiveDataModel(schema);
    5.73 +                doc.dataModels.put(schema, dataModel);
    5.74 +            }
    5.75 +        }
    5.76 +        return doc;
    5.77 +    }
    5.78 +
    5.79 +    public static DocumentModel createFictiveDocumentModelWith(
    5.80 +            String... schemas) {
    5.81 +        return createFictiveDocumentModelWith(Arrays.asList(schemas));
    5.82 +    }
    5.83 +
    5.84 +    protected FictiveDocumentModel() {
    5.85 +    }
    5.86 +
    5.87 +    public String[] getDeclaredSchemas() {
    5.88 +        Set<String> keys = dataModels.keySet();
    5.89 +        return keys.toArray(new String[keys.size()]);
    5.90 +    }
    5.91 +
    5.92 +    public Object getProperty(String schemaName, String name)
    5.93 +            throws ClientException {
    5.94 +        DataModel dm = dataModels.get(schemaName);
    5.95 +        return dm != null ? dm.getData(name) : null;
    5.96 +    }
    5.97 +
    5.98 +    public void setProperty(String schemaName, String name, Object value)
    5.99 +            throws ClientException {
   5.100 +        if (name.contains(":")) {
   5.101 +            name = name.substring(name.indexOf(":"), name.length());
   5.102 +        }
   5.103 +        dataModels.get(schemaName).setData(name, value);
   5.104 +    }
   5.105 +
   5.106 +    public Map<String, Object> getProperties(String schemaName)
   5.107 +            throws ClientException {
   5.108 +        return dataModels.get(schemaName).getMap();
   5.109 +    }
   5.110 +
   5.111 +    public void setProperties(String schemaName, Map<String, Object> data)
   5.112 +            throws ClientException {
   5.113 +        dataModels.get(schemaName).setMap(data);
   5.114 +    }
   5.115 +
   5.116 +    public ScopedMap getContextData() {
   5.117 +        return contextData;
   5.118 +    }
   5.119 +
   5.120 +    public Serializable getContextData(ScopeType scope, String key) {
   5.121 +        return contextData.getScopedValue(scope, key);
   5.122 +    }
   5.123 +
   5.124 +    public void putContextData(ScopeType scope, String key, Serializable value) {
   5.125 +        contextData.putScopedValue(scope, key, value);
   5.126 +    }
   5.127 +
   5.128 +    public Serializable getContextData(String key) {
   5.129 +        return contextData.getScopedValue(key);
   5.130 +    }
   5.131 +
   5.132 +    public void putContextData(String key, Serializable value) {
   5.133 +        contextData.putScopedValue(key, value);
   5.134 +    }
   5.135 +
   5.136 +    public void copyContextData(DocumentModel otherDocument) {
   5.137 +        ScopedMap otherMap = otherDocument.getContextData();
   5.138 +        if (otherMap != null) {
   5.139 +            contextData.putAll(otherMap);
   5.140 +        }
   5.141 +    }
   5.142 +
   5.143 +    public Property getProperty(String xpath) throws PropertyException,
   5.144 +            ClientException {
   5.145 +        Path path = new Path(xpath);
   5.146 +        if (path.segmentCount() == 0) {
   5.147 +            throw new PropertyNotFoundException(xpath, "Schema not specified");
   5.148 +        }
   5.149 +        String segment = path.segment(0);
   5.150 +        int p = segment.indexOf(':');
   5.151 +        if (p == -1) { // support also other schema paths? like schema.property
   5.152 +            // allow also unprefixed schemas -> make a search for the first
   5.153 +            // matching schema having a property with same name as path segment
   5.154 +            // 0
   5.155 +            DocumentPart[] parts = getParts();
   5.156 +            for (DocumentPart part : parts) {
   5.157 +                if (part.getSchema().hasField(segment)) {
   5.158 +                    return part.resolvePath(path.toString());
   5.159 +                }
   5.160 +            }
   5.161 +            // could not find any matching schema
   5.162 +            throw new PropertyNotFoundException(xpath, "Schema not specified");
   5.163 +        }
   5.164 +        String prefix = segment.substring(0, p);
   5.165 +        SchemaManager mgr = Framework.getLocalService(SchemaManager.class);
   5.166 +        Schema schema = mgr.getSchemaFromPrefix(prefix);
   5.167 +        if (schema == null) {
   5.168 +            schema = mgr.getSchema(prefix);
   5.169 +            if (schema == null) {
   5.170 +                throw new PropertyNotFoundException(xpath,
   5.171 +                        "Could not find registered schema with prefix: "
   5.172 +                                + prefix);
   5.173 +            }
   5.174 +        }
   5.175 +        String[] segments = path.segments();
   5.176 +        segments[0] = segments[0].substring(p + 1);
   5.177 +        path = Path.createFromSegments(segments);
   5.178 +
   5.179 +        DocumentPart part = DefaultPropertyFactory.newDocumentPart(schema);
   5.180 +        part.init((Serializable) dataModels.get(schema.getName()).getMap());
   5.181 +        if (part == null) {
   5.182 +            throw new PropertyNotFoundException(
   5.183 +                    xpath,
   5.184 +                    String.format(
   5.185 +                            "Document '%s' with title '%s' and type '%s' does not have any schema with prefix '%s'",
   5.186 +                            getRef(), getTitle(), getType(), prefix));
   5.187 +        }
   5.188 +        return part.resolvePath(path.toString());
   5.189 +    }
   5.190 +
   5.191 +    public Serializable getPropertyValue(String xpath)
   5.192 +            throws PropertyException, ClientException {
   5.193 +        return getProperty(xpath).getValue();
   5.194 +    }
   5.195 +
   5.196 +    public void setPropertyValue(String xpath, Serializable value)
   5.197 +            throws PropertyException, ClientException {
   5.198 +        getProperty(xpath).setValue(value);
   5.199 +    }
   5.200 +
   5.201 +    public DocumentType getDocumentType() {
   5.202 +        throw new UnsupportedOperationException();
   5.203 +    }
   5.204 +
   5.205 +    public String getSessionId() {
   5.206 +        throw new UnsupportedOperationException();
   5.207 +    }
   5.208 +
   5.209 +    public CoreSession getCoreSession() {
   5.210 +        throw new UnsupportedOperationException();
   5.211 +    }
   5.212 +
   5.213 +    public DocumentRef getRef() {
   5.214 +        throw new UnsupportedOperationException();
   5.215 +    }
   5.216 +
   5.217 +    public DocumentRef getParentRef() {
   5.218 +        throw new UnsupportedOperationException();
   5.219 +    }
   5.220 +
   5.221 +    public String getId() {
   5.222 +        throw new UnsupportedOperationException();
   5.223 +    }
   5.224 +
   5.225 +    public String getName() {
   5.226 +        throw new UnsupportedOperationException();
   5.227 +    }
   5.228 +
   5.229 +    public String getTitle() throws ClientException {
   5.230 +        throw new UnsupportedOperationException();
   5.231 +    }
   5.232 +
   5.233 +    public String getPathAsString() {
   5.234 +        throw new UnsupportedOperationException();
   5.235 +    }
   5.236 +
   5.237 +    public Path getPath() {
   5.238 +        throw new UnsupportedOperationException();
   5.239 +    }
   5.240 +
   5.241 +    public String getType() {
   5.242 +        throw new UnsupportedOperationException();
   5.243 +    }
   5.244 +
   5.245 +    public Set<String> getDeclaredFacets() {
   5.246 +        throw new UnsupportedOperationException();
   5.247 +    }
   5.248 +
   5.249 +    public Collection<DataModel> getDataModelsCollection() {
   5.250 +        throw new UnsupportedOperationException();
   5.251 +    }
   5.252 +
   5.253 +    public DataModelMap getDataModels() {
   5.254 +        throw new UnsupportedOperationException();
   5.255 +    }
   5.256 +
   5.257 +    public DataModel getDataModel(String schema) throws ClientException {
   5.258 +        throw new UnsupportedOperationException();
   5.259 +    }
   5.260 +
   5.261 +    public void setPathInfo(String parentPath, String name) {
   5.262 +        throw new UnsupportedOperationException();
   5.263 +    }
   5.264 +
   5.265 +    public String getLock() {
   5.266 +        throw new UnsupportedOperationException();
   5.267 +    }
   5.268 +
   5.269 +    public boolean isLocked() {
   5.270 +        throw new UnsupportedOperationException();
   5.271 +    }
   5.272 +
   5.273 +    public void setLock(String key) throws ClientException {
   5.274 +        throw new UnsupportedOperationException();
   5.275 +    }
   5.276 +
   5.277 +    public void unlock() throws ClientException {
   5.278 +        throw new UnsupportedOperationException();
   5.279 +    }
   5.280 +
   5.281 +    public ACP getACP() throws ClientException {
   5.282 +        throw new UnsupportedOperationException();
   5.283 +    }
   5.284 +
   5.285 +    public void setACP(ACP acp, boolean overwrite) throws ClientException {
   5.286 +        throw new UnsupportedOperationException();
   5.287 +    }
   5.288 +
   5.289 +    public boolean hasSchema(String schema) {
   5.290 +        throw new UnsupportedOperationException();
   5.291 +    }
   5.292 +
   5.293 +    public boolean hasFacet(String facet) {
   5.294 +        throw new UnsupportedOperationException();
   5.295 +    }
   5.296 +
   5.297 +    public boolean isFolder() {
   5.298 +        throw new UnsupportedOperationException();
   5.299 +    }
   5.300 +
   5.301 +    public boolean isVersionable() {
   5.302 +        throw new UnsupportedOperationException();
   5.303 +    }
   5.304 +
   5.305 +    public boolean isDownloadable() throws ClientException {
   5.306 +        throw new UnsupportedOperationException();
   5.307 +    }
   5.308 +
   5.309 +    public boolean isVersion() {
   5.310 +        throw new UnsupportedOperationException();
   5.311 +    }
   5.312 +
   5.313 +    public boolean isProxy() {
   5.314 +        throw new UnsupportedOperationException();
   5.315 +    }
   5.316 +
   5.317 +    public boolean isImmutable() {
   5.318 +        throw new UnsupportedOperationException();
   5.319 +    }
   5.320 +
   5.321 +    public <T> T getAdapter(Class<T> itf) {
   5.322 +        throw new UnsupportedOperationException();
   5.323 +    }
   5.324 +
   5.325 +    public <T> T getAdapter(Class<T> itf, boolean refreshCache) {
   5.326 +        throw new UnsupportedOperationException();
   5.327 +    }
   5.328 +
   5.329 +    public String getCurrentLifeCycleState() throws ClientException {
   5.330 +        throw new UnsupportedOperationException();
   5.331 +    }
   5.332 +
   5.333 +    public String getLifeCyclePolicy() throws ClientException {
   5.334 +        throw new UnsupportedOperationException();
   5.335 +    }
   5.336 +
   5.337 +    public boolean followTransition(String transition) throws ClientException {
   5.338 +        throw new UnsupportedOperationException();
   5.339 +    }
   5.340 +
   5.341 +    public Collection<String> getAllowedStateTransitions()
   5.342 +            throws ClientException {
   5.343 +        throw new UnsupportedOperationException();
   5.344 +    }
   5.345 +
   5.346 +    public void copyContent(DocumentModel sourceDoc) throws ClientException {
   5.347 +        throw new UnsupportedOperationException();
   5.348 +    }
   5.349 +
   5.350 +    public String getRepositoryName() {
   5.351 +        throw new UnsupportedOperationException();
   5.352 +    }
   5.353 +
   5.354 +    public String getCacheKey() throws ClientException {
   5.355 +        throw new UnsupportedOperationException();
   5.356 +    }
   5.357 +
   5.358 +    public String getSourceId() {
   5.359 +        throw new UnsupportedOperationException();
   5.360 +    }
   5.361 +
   5.362 +    public String getVersionLabel() {
   5.363 +        throw new UnsupportedOperationException();
   5.364 +    }
   5.365 +
   5.366 +    public Map<String, Serializable> getPrefetch() {
   5.367 +        throw new UnsupportedOperationException();
   5.368 +    }
   5.369 +
   5.370 +    public void prefetchProperty(String id, Object value) {
   5.371 +        throw new UnsupportedOperationException();
   5.372 +    }
   5.373 +
   5.374 +    public void prefetchCurrentLifecycleState(String lifecycle) {
   5.375 +        throw new UnsupportedOperationException();
   5.376 +    }
   5.377 +
   5.378 +    public void prefetchLifeCyclePolicy(String lifeCyclePolicy) {
   5.379 +        throw new UnsupportedOperationException();
   5.380 +    }
   5.381 +
   5.382 +    public boolean isLifeCycleLoaded() {
   5.383 +        throw new UnsupportedOperationException();
   5.384 +    }
   5.385 +
   5.386 +    public <T extends Serializable> T getSystemProp(String systemProperty,
   5.387 +            Class<T> type) throws ClientException, DocumentException {
   5.388 +        throw new UnsupportedOperationException();
   5.389 +    }
   5.390 +
   5.391 +    public DocumentPart getPart(String schema) throws ClientException {
   5.392 +        throw new UnsupportedOperationException();
   5.393 +    }
   5.394 +
   5.395 +    public DocumentPart[] getParts() throws ClientException {
   5.396 +        throw new UnsupportedOperationException();
   5.397 +    }
   5.398 +
   5.399 +    public long getFlags() {
   5.400 +        throw new UnsupportedOperationException();
   5.401 +    }
   5.402 +
   5.403 +    public void reset() {
   5.404 +        throw new UnsupportedOperationException();
   5.405 +    }
   5.406 +
   5.407 +    public void refresh(int refreshFlags, String[] schemas)
   5.408 +            throws ClientException {
   5.409 +        throw new UnsupportedOperationException();
   5.410 +    }
   5.411 +
   5.412 +    public void refresh() throws ClientException {
   5.413 +        throw new UnsupportedOperationException();
   5.414 +    }
   5.415 +
   5.416 +    public DocumentModel clone() throws CloneNotSupportedException {
   5.417 +        throw new UnsupportedOperationException();
   5.418 +    }
   5.419 +
   5.420 +}
     6.1 new file mode 100644
     6.2 --- /dev/null
     6.3 +++ b/nuxeo-platform-webapp-base/src/test/java/org/nuxeo/ecm/webapp/bulkedit/TestBulkEditHelper.java
     6.4 @@ -0,0 +1,149 @@
     6.5 +/*
     6.6 + * (C) Copyright 2009 Nuxeo SA (http://nuxeo.com/) and contributors.
     6.7 + *
     6.8 + * All rights reserved. This program and the accompanying materials
     6.9 + * are made available under the terms of the GNU Lesser General Public License
    6.10 + * (LGPL) version 2.1 which accompanies this distribution, and is available at
    6.11 + * http://www.gnu.org/licenses/lgpl.html
    6.12 + *
    6.13 + * This library is distributed in the hope that it will be useful,
    6.14 + * but WITHOUT ANY WARRANTY; without even the implied warranty of
    6.15 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
    6.16 + * Lesser General Public License for more details.
    6.17 + *
    6.18 + * Contributors:
    6.19 + *     Thomas Roger
    6.20 + */
    6.21 +
    6.22 +package org.nuxeo.ecm.webapp.bulkedit;
    6.23 +
    6.24 +import java.util.Arrays;
    6.25 +import java.util.List;
    6.26 +
    6.27 +import org.junit.Test;
    6.28 +import org.junit.runner.RunWith;
    6.29 +import org.nuxeo.common.collections.ScopedMap;
    6.30 +import org.nuxeo.ecm.core.api.ClientException;
    6.31 +import org.nuxeo.ecm.core.api.CoreSession;
    6.32 +import org.nuxeo.ecm.core.api.DocumentModel;
    6.33 +import org.nuxeo.ecm.core.test.CoreFeature;
    6.34 +import org.nuxeo.ecm.core.test.annotations.BackendType;
    6.35 +import org.nuxeo.ecm.core.test.annotations.RepositoryConfig;
    6.36 +import org.nuxeo.ecm.platform.types.TypeManager;
    6.37 +import org.nuxeo.runtime.test.runner.Deploy;
    6.38 +import org.nuxeo.runtime.test.runner.Features;
    6.39 +import org.nuxeo.runtime.test.runner.FeaturesRunner;
    6.40 +
    6.41 +import com.google.inject.Inject;
    6.42 +
    6.43 +import static org.junit.Assert.assertEquals;
    6.44 +import static org.junit.Assert.assertFalse;
    6.45 +import static org.junit.Assert.assertNotNull;
    6.46 +import static org.junit.Assert.assertTrue;
    6.47 +
    6.48 +/**
    6.49 + * @author <a href="mailto:troger@nuxeo.com">Thomas Roger</a>
    6.50 + */
    6.51 +@RunWith(FeaturesRunner.class)
    6.52 +@Features(CoreFeature.class)
    6.53 +@RepositoryConfig(type = BackendType.H2, user = "Administrator")
    6.54 +@Deploy( { "org.nuxeo.ecm.platform.types.api",
    6.55 +        "org.nuxeo.ecm.platform.types.core", "org.nuxeo.ecm.webapp.base" })
    6.56 +public class TestBulkEditHelper {
    6.57 +
    6.58 +    @Inject
    6.59 +    protected CoreSession session;
    6.60 +
    6.61 +    @Inject
    6.62 +    protected TypeManager typeManager;
    6.63 +
    6.64 +    @Test
    6.65 +    public void testCommonSchemas() throws Exception {
    6.66 +        List<DocumentModel> docs = createTestDocuments();
    6.67 +        List<String> commonSchemas = BulkEditHelper.getCommonSchemas(docs);
    6.68 +        assertFalse(commonSchemas.isEmpty());
    6.69 +        assertEquals(4, commonSchemas.size());
    6.70 +        assertTrue(commonSchemas.contains("uid"));
    6.71 +        assertTrue(commonSchemas.contains("dublincore"));
    6.72 +        assertTrue(commonSchemas.contains("common"));
    6.73 +        assertTrue(commonSchemas.contains("files"));
    6.74 +        assertFalse(commonSchemas.contains("note"));
    6.75 +        assertFalse(commonSchemas.contains("file"));
    6.76 +    }
    6.77 +
    6.78 +    protected List<DocumentModel> createTestDocuments() throws ClientException {
    6.79 +        DocumentModel file = session.createDocumentModel("/", "testFile",
    6.80 +                "File");
    6.81 +        file.setPropertyValue("dc:title", "testTitle");
    6.82 +        file = session.createDocument(file);
    6.83 +        assertNotNull(file);
    6.84 +        file = session.saveDocument(file);
    6.85 +
    6.86 +        DocumentModel note = session.createDocumentModel("/", "testFile",
    6.87 +                "Note");
    6.88 +        note.setPropertyValue("dc:title", "testNote");
    6.89 +        note = session.createDocument(note);
    6.90 +        assertNotNull(note);
    6.91 +        note = session.saveDocument(note);
    6.92 +        session.save();
    6.93 +
    6.94 +        return Arrays.asList(file,
    6.95 +                note);
    6.96 +    }
    6.97 +
    6.98 +    @Test
    6.99 +    public void testCommonLayouts() throws Exception {
   6.100 +        List<DocumentModel> docs = createTestDocuments();
   6.101 +        List<String> commonLayouts = BulkEditHelper.getCommonLayouts(
   6.102 +                typeManager, docs);
   6.103 +        assertFalse(commonLayouts.isEmpty());
   6.104 +        assertEquals(2, commonLayouts.size());
   6.105 +        assertTrue(commonLayouts.contains("heading"));
   6.106 +        assertTrue(commonLayouts.contains("dublincore"));
   6.107 +        assertFalse(commonLayouts.contains("note"));
   6.108 +        assertFalse(commonLayouts.contains("file"));
   6.109 +    }
   6.110 +
   6.111 +    @Test
   6.112 +    public void testGetPropertiesToCopy() throws Exception {
   6.113 +        DocumentModel doc = FictiveDocumentModel.createFictiveDocumentModelWith("dublincore");
   6.114 +        ScopedMap map = doc.getContextData();
   6.115 +        map.put(BulkEditHelper.BULK_EDIT_PREFIX + "dc:title", true);
   6.116 +        map.put(BulkEditHelper.BULK_EDIT_PREFIX + "dc:description", false);
   6.117 +        map.put(BulkEditHelper.BULK_EDIT_PREFIX + "dc:coverage dc:subjects", true);
   6.118 +        map.put(BulkEditHelper.BULK_EDIT_PREFIX + "dc:creator", true);
   6.119 +
   6.120 +        List<String> propertiesToCopy = BulkEditHelper.getPropertiesToCopy(doc);
   6.121 +        assertEquals(4, propertiesToCopy.size());
   6.122 +        assertTrue(propertiesToCopy.contains("dc:title"));
   6.123 +        assertTrue(propertiesToCopy.contains("dc:coverage"));
   6.124 +        assertTrue(propertiesToCopy.contains("dc:subjects"));
   6.125 +        assertTrue(propertiesToCopy.contains("dc:creator"));
   6.126 +        assertFalse(propertiesToCopy.contains("dc:description"));
   6.127 +    }
   6.128 +
   6.129 +    @Test
   6.130 +    public void testCopyMetadata() throws Exception {
   6.131 +        List<DocumentModel> docs = createTestDocuments();
   6.132 +        List<String> commonSchemas = BulkEditHelper.getCommonSchemas(docs);
   6.133 +        DocumentModel sourceDoc = FictiveDocumentModel.createFictiveDocumentModelWith(commonSchemas);
   6.134 +        sourceDoc.setProperty("dublincore", "title", "new title");
   6.135 +        sourceDoc.setProperty("dublincore", "description", "new description");
   6.136 +        sourceDoc.setProperty("dublincore", "creator", "new creator");
   6.137 +        sourceDoc.setProperty("dublincore", "source", "new source");
   6.138 +        ScopedMap map = sourceDoc.getContextData();
   6.139 +        map.put(BulkEditHelper.BULK_EDIT_PREFIX + "dc:title", true);
   6.140 +        map.put(BulkEditHelper.BULK_EDIT_PREFIX + "dc:description", false);
   6.141 +        map.put(BulkEditHelper.BULK_EDIT_PREFIX + "dc:creator", true);
   6.142 +        map.put(BulkEditHelper.BULK_EDIT_PREFIX + "dc:source", false);
   6.143 +
   6.144 +        BulkEditHelper.copyMetadata(session, sourceDoc, docs);
   6.145 +        for (DocumentModel doc : docs) {
   6.146 +            assertEquals("new title", doc.getPropertyValue("dc:title"));
   6.147 +            assertEquals("new creator", doc.getPropertyValue("dc:creator"));
   6.148 +            assertFalse("new description".equals(doc.getPropertyValue("dc:description")));
   6.149 +            assertFalse("new source".equals(doc.getPropertyValue("dc:source")));
   6.150 +        }
   6.151 +    }
   6.152 +
   6.153 +}
     7.1 new file mode 100644
     7.2 --- /dev/null
     7.3 +++ b/nuxeo-platform-webapp-base/src/test/resources/log4j.properties
     7.4 @@ -0,0 +1,4 @@
     7.5 +log4j.rootLogger=WARN, CONSOLE
     7.6 +log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
     7.7 +log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
     7.8 +log4j.appender.CONSOLE.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p [%C{1}] %m%n
     8.1 --- a/pom.xml
     8.2 +++ b/pom.xml
     8.3 @@ -244,6 +244,18 @@
     8.4          <version>${nuxeo.runtime.version}</version>
     8.5          <scope>test</scope>
     8.6        </dependency>
     8.7 +      <dependency>
     8.8 +        <groupId>org.nuxeo.ecm.core</groupId>
     8.9 +        <artifactId>nuxeo-core-test</artifactId>
    8.10 +        <version>${nuxeo.core.version}</version>
    8.11 +        <scope>test</scope>
    8.12 +      </dependency>
    8.13 +      <dependency>
    8.14 +        <groupId>org.nuxeo.ecm.platform</groupId>
    8.15 +        <artifactId>nuxeo-platform-types-core</artifactId>
    8.16 +        <version>${nuxeo.services.version}</version>
    8.17 +        <scope>test</scope>
    8.18 +      </dependency>
    8.19      </dependencies>
    8.20    </dependencyManagement>
    8.21