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