001/*
002 * (C) Copyright 2006-2011 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     bstefanescu
018 */
019package org.nuxeo.ecm.automation.core.util;
020
021import java.io.IOException;
022import java.io.InputStream;
023import java.io.Serializable;
024import java.util.ArrayList;
025import java.util.Calendar;
026import java.util.HashMap;
027import java.util.Map;
028
029import org.nuxeo.common.utils.StringUtils;
030import org.nuxeo.ecm.core.api.Blob;
031import org.nuxeo.ecm.core.api.CoreSession;
032import org.nuxeo.ecm.core.api.DocumentModel;
033import org.nuxeo.ecm.core.api.NuxeoException;
034import org.nuxeo.ecm.core.api.PropertyException;
035import org.nuxeo.ecm.core.api.model.Property;
036import org.nuxeo.ecm.core.api.model.impl.ListProperty;
037import org.nuxeo.ecm.core.api.security.ACE;
038import org.nuxeo.ecm.core.api.security.ACL;
039import org.nuxeo.ecm.core.api.security.impl.ACLImpl;
040import org.nuxeo.ecm.core.api.security.impl.ACPImpl;
041import org.nuxeo.ecm.core.schema.types.ComplexType;
042import org.nuxeo.ecm.core.schema.types.ListType;
043import org.nuxeo.ecm.core.schema.types.SimpleType;
044import org.nuxeo.ecm.core.schema.types.Type;
045import org.nuxeo.ecm.core.schema.types.primitives.BinaryType;
046import org.nuxeo.ecm.core.schema.types.primitives.BooleanType;
047import org.nuxeo.ecm.core.schema.types.primitives.DateType;
048import org.nuxeo.ecm.core.schema.types.primitives.DoubleType;
049import org.nuxeo.ecm.core.schema.types.primitives.IntegerType;
050import org.nuxeo.ecm.core.schema.types.primitives.LongType;
051import org.nuxeo.ecm.core.schema.types.primitives.StringType;
052
053/**
054 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
055 */
056public class DocumentHelper {
057
058    private DocumentHelper() {
059    }
060
061    /**
062     * Saves the document and clear context data to avoid incrementing version in next operations if not needed.
063     */
064    public static DocumentModel saveDocument(CoreSession session, DocumentModel doc) {
065        doc = session.saveDocument(doc);
066        return session.getDocument(doc.getRef());
067    }
068
069    /**
070     * Removes a property from a document given the xpath. If the xpath points to a list property the list will be
071     * cleared. If the path points to a blob in a list the property is removed from the list. Otherwise the xpath should
072     * point to a non list property that will be removed.
073     */
074    public static void removeProperty(DocumentModel doc, String xpath) {
075        Property p = doc.getProperty(xpath);
076        if (p instanceof ListProperty) {
077            ((ListProperty) p).clear();
078        } else {
079            Property pp = p.getParent();
080            if (pp != null && pp.isList()) { // remove list entry
081                ((ListProperty) pp).remove(p);
082            } else {
083                p.remove();
084            }
085        }
086    }
087
088    /**
089     * Given a document property, updates its value with the given blob. The property can be a blob list or a blob. If a
090     * blob list the blob is appended to the list, if a blob then it will be set as the property value. Both blob list
091     * formats are supported: the file list (blob holder list) and simple blob list.
092     */
093    public static void addBlob(Property p, Blob blob) throws PropertyException {
094        if (p.isList()) {
095            // detect if a list of simple blobs or a list of files (blob
096            // holder)
097            Type ft = ((ListProperty) p).getType().getFieldType();
098            if (ft.isComplexType() && ((ComplexType) ft).getFieldsCount() == 2) {
099                p.addValue(createBlobHolderMap(blob));
100            } else {
101                p.addValue(blob);
102            }
103        } else {
104            p.setValue(blob);
105        }
106    }
107
108    public static HashMap<String, Serializable> createBlobHolderMap(Blob blob) {
109        HashMap<String, Serializable> map = new HashMap<String, Serializable>();
110        map.put("file", (Serializable) blob);
111        map.put("filename", blob.getFilename());
112        return map;
113    }
114
115    public static void setProperties(CoreSession session, DocumentModel doc, Properties properties)
116            throws IOException, PropertyException {
117        if (properties instanceof DataModelProperties) {
118            DataModelProperties dataModelProperties = (DataModelProperties) properties;
119            for (Map.Entry<String, Serializable> entry : dataModelProperties.getMap().entrySet()) {
120                doc.setPropertyValue(entry.getKey(), entry.getValue());
121            }
122        } else {
123            for (Map.Entry<String, String> entry : properties.entrySet()) {
124                String key = entry.getKey();
125                String value = entry.getValue();
126                setProperty(session, doc, key, value);
127            }
128        }
129    }
130
131    /**
132     * Sets the properties given as a map of xpath:value to the given document. There is one special property: ecm:acl
133     * that can be used to set the local acl. The format of this property value is: [string username]:[string
134     * permission]:[boolean grant], [string username]:[string permission]:[boolean grant], ... TODO list properties are
135     * not yet supported
136     */
137    public static void setProperties(CoreSession session, DocumentModel doc, Map<String, String> values)
138            throws IOException {
139        for (Map.Entry<String, String> entry : values.entrySet()) {
140            String key = entry.getKey();
141            String value = entry.getValue();
142            setProperty(session, doc, key, value);
143        }
144    }
145
146    public static void setProperty(CoreSession session, DocumentModel doc, String key, String value)
147            throws IOException {
148        setProperty(session, doc, key, value, false);
149    }
150
151    protected static void setLocalAcl(CoreSession session, DocumentModel doc, String value) {
152        ACPImpl acp = new ACPImpl();
153        ACLImpl acl = new ACLImpl(ACL.LOCAL_ACL);
154        acp.addACL(acl);
155        String[] entries = StringUtils.split(value, ',', true);
156        if (entries.length == 0) {
157            return;
158        }
159        for (String entry : entries) {
160            String[] ace = StringUtils.split(entry, ':', true);
161            acl.add(new ACE(ace[0], ace[1], Boolean.parseBoolean(ace[2])));
162        }
163        session.setACP(doc.getRef(), acp, false);
164    }
165
166    /**
167     * Read an encoded string list as a comma separated list. To use comma inside list element values you need to escape
168     * them using '\'. If the given type is different from {@link StringType#ID} then array elements will be converted
169     * to the actual type.
170     *
171     * @param value
172     * @param type
173     * @return
174     */
175    public static Object readStringList(String value, SimpleType type) {
176        if (!type.isPrimitive()) {
177            return readStringList(value, type.getPrimitiveType());
178        }
179        String[] ar = readStringList(value);
180        if (ar == null) {
181            return null;
182        }
183        if (StringType.INSTANCE == type) {
184            return ar;
185        } else if (DateType.INSTANCE == type) {
186            Calendar[] r = new Calendar[ar.length];
187            for (int i = 0; i < r.length; i++) {
188                r[i] = (Calendar) type.decode(ar[i]);
189            }
190            return r;
191        } else if (LongType.INSTANCE == type) {
192            Long[] r = new Long[ar.length];
193            for (int i = 0; i < r.length; i++) {
194                r[i] = (Long) type.decode(ar[i]);
195            }
196            return r;
197        } else if (IntegerType.INSTANCE == type) {
198            Integer[] r = new Integer[ar.length];
199            for (int i = 0; i < r.length; i++) {
200                r[i] = (Integer) type.decode(ar[i]);
201            }
202            return r;
203        } else if (DoubleType.INSTANCE == type) {
204            Double[] r = new Double[ar.length];
205            for (int i = 0; i < r.length; i++) {
206                r[i] = (Double) type.decode(ar[i]);
207            }
208            return r;
209        } else if (BooleanType.INSTANCE == type) {
210            Boolean[] r = new Boolean[ar.length];
211            for (int i = 0; i < r.length; i++) {
212                r[i] = (Boolean) type.decode(ar[i]);
213            }
214            return r;
215        } else if (BinaryType.INSTANCE == type) {
216            InputStream[] r = new InputStream[ar.length];
217            for (int i = 0; i < r.length; i++) {
218                r[i] = (InputStream) type.decode(ar[i]);
219            }
220            return r;
221        }
222        throw new IllegalArgumentException(
223                "Unsupported type when updating document properties from string representation: " + type);
224    }
225
226    /**
227     * Read an encoded string list as a comma separated list. To use comma inside list element values you need to escape
228     * them using '\'.
229     *
230     * @param value
231     * @return
232     */
233    public static String[] readStringList(String value) {
234        if (value == null) {
235            return null;
236        }
237        if (value.length() == 0) {
238            return new String[0];
239        }
240        ArrayList<String> result = new ArrayList<String>();
241        char[] chars = value.toCharArray();
242        StringBuilder buf = new StringBuilder();
243        boolean esc = false;
244        for (int i = 0; i < chars.length; i++) {
245            char c = chars[i];
246            if (c == '\\') {
247                if (esc) {
248                    buf.append('\\');
249                    esc = false;
250                } else {
251                    esc = true;
252                }
253            } else if (c == ',') {
254                if (esc) {
255                    buf.append(',');
256                    esc = false;
257                } else {
258                    result.add(buf.toString());
259                    buf = new StringBuilder();
260                }
261            } else {
262                buf.append(c);
263            }
264        }
265        result.add(buf.toString());
266        return result.toArray(new String[result.size()]);
267    }
268
269    /**
270     * Sets the properties of a document based on their JSON representation (especially for scalar lists).
271     *
272     * @param session
273     * @param doc
274     * @param properties
275     * @throws IOException
276     * @since 5.9.2
277     */
278    public static void setJSONProperties(CoreSession session, DocumentModel doc, Properties properties)
279            throws IOException {
280
281        for (Map.Entry<String, String> entry : properties.entrySet()) {
282            String key = entry.getKey();
283            String value = entry.getValue();
284            setProperty(session, doc, key, value, true);
285        }
286    }
287
288    /**
289     * @param session
290     * @param doc
291     * @param key
292     * @param value
293     * @param decodeStringListAsJSON
294     * @throws IOException
295     * @since 5.9.2
296     */
297    public static void setProperty(CoreSession session, DocumentModel doc, String key, String value,
298            boolean decodeStringListAsJSON) throws IOException {
299        if ("ecm:acl".equals(key)) {
300            setLocalAcl(session, doc, value);
301        }
302        Property p = doc.getProperty(key);
303        if (value == null || value.length() == 0) {
304            p.setValue(null);
305            return;
306        }
307        Type type = p.getField().getType();
308        if (!type.isSimpleType()) {
309            if (type.isListType()) {
310                ListType ltype = (ListType) type;
311                if (ltype.isScalarList() && !decodeStringListAsJSON) {
312                    p.setValue(readStringList(value, (SimpleType) ltype.getFieldType()));
313                    return;
314                } else {
315                    Object val = ComplexTypeJSONDecoder.decodeList(ltype, value);
316                    p.setValue(val);
317                    return;
318                }
319            } else if (type.isComplexType()) {
320                Object val = ComplexTypeJSONDecoder.decode((ComplexType) type, value);
321                p.setValue(val);
322                return;
323            }
324            throw new NuxeoException("Property type is not supported by this operation");
325        } else {
326            p.setValue(((SimpleType) type).getPrimitiveType().decode(value));
327        }
328    }
329
330}