001/*
002 * (C) Copyright 2006-2016 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() == 1) {
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<>();
110        map.put("file", (Serializable) blob);
111        return map;
112    }
113
114    public static void setProperties(CoreSession session, DocumentModel doc, Properties properties)
115            throws IOException, PropertyException {
116        if (properties instanceof DataModelProperties) {
117            DataModelProperties dataModelProperties = (DataModelProperties) properties;
118            for (Map.Entry<String, Serializable> entry : dataModelProperties.getMap().entrySet()) {
119                doc.setPropertyValue(entry.getKey(), entry.getValue());
120            }
121        }
122        for (Map.Entry<String, String> entry : properties.entrySet()) {
123            String key = entry.getKey();
124            String value = entry.getValue();
125            setProperty(session, doc, key, value);
126        }
127    }
128
129    /**
130     * Sets the properties given as a map of xpath:value to the given document. There is one special property: ecm:acl
131     * that can be used to set the local acl. The format of this property value is: [string username]:[string
132     * permission]:[boolean grant], [string username]:[string permission]:[boolean grant], ... TODO list properties are
133     * not yet supported
134     */
135    public static void setProperties(CoreSession session, DocumentModel doc, Map<String, String> values)
136            throws IOException {
137        for (Map.Entry<String, String> entry : values.entrySet()) {
138            String key = entry.getKey();
139            String value = entry.getValue();
140            setProperty(session, doc, key, value);
141        }
142    }
143
144    public static void setProperty(CoreSession session, DocumentModel doc, String key, String value)
145            throws IOException {
146        setProperty(session, doc, key, value, false);
147    }
148
149    protected static void setLocalAcl(CoreSession session, DocumentModel doc, String value) {
150        ACPImpl acp = new ACPImpl();
151        ACLImpl acl = new ACLImpl(ACL.LOCAL_ACL);
152        acp.addACL(acl);
153        String[] entries = StringUtils.split(value, ',', true);
154        if (entries.length == 0) {
155            return;
156        }
157        for (String entry : entries) {
158            String[] ace = StringUtils.split(entry, ':', true);
159            acl.add(new ACE(ace[0], ace[1], Boolean.parseBoolean(ace[2])));
160        }
161        session.setACP(doc.getRef(), acp, false);
162    }
163
164    /**
165     * Read an encoded string list as a comma separated list. To use comma inside list element values you need to escape
166     * them using '\'. If the given type is different from {@link StringType#ID} then array elements will be converted
167     * to the actual type.
168     */
169    public static Object readStringList(String value, SimpleType type) {
170        if (!type.isPrimitive()) {
171            return readStringList(value, type.getPrimitiveType());
172        }
173        String[] ar = readStringList(value);
174        if (ar == null) {
175            return null;
176        }
177        if (StringType.INSTANCE == type) {
178            return ar;
179        } else if (DateType.INSTANCE == type) {
180            Calendar[] r = new Calendar[ar.length];
181            for (int i = 0; i < r.length; i++) {
182                r[i] = (Calendar) type.decode(ar[i]);
183            }
184            return r;
185        } else if (LongType.INSTANCE == type) {
186            Long[] r = new Long[ar.length];
187            for (int i = 0; i < r.length; i++) {
188                r[i] = (Long) type.decode(ar[i]);
189            }
190            return r;
191        } else if (IntegerType.INSTANCE == type) {
192            Integer[] r = new Integer[ar.length];
193            for (int i = 0; i < r.length; i++) {
194                r[i] = (Integer) type.decode(ar[i]);
195            }
196            return r;
197        } else if (DoubleType.INSTANCE == type) {
198            Double[] r = new Double[ar.length];
199            for (int i = 0; i < r.length; i++) {
200                r[i] = (Double) type.decode(ar[i]);
201            }
202            return r;
203        } else if (BooleanType.INSTANCE == type) {
204            Boolean[] r = new Boolean[ar.length];
205            for (int i = 0; i < r.length; i++) {
206                r[i] = (Boolean) type.decode(ar[i]);
207            }
208            return r;
209        } else if (BinaryType.INSTANCE == type) {
210            InputStream[] r = new InputStream[ar.length];
211            for (int i = 0; i < r.length; i++) {
212                r[i] = (InputStream) type.decode(ar[i]);
213            }
214            return r;
215        }
216        throw new IllegalArgumentException(
217                "Unsupported type when updating document properties from string representation: " + type);
218    }
219
220    /**
221     * Read an encoded string list as a comma separated list. To use comma inside list element values you need to escape
222     * them using '\'.
223     */
224    public static String[] readStringList(String value) {
225        if (value == null) {
226            return null;
227        }
228        if (value.length() == 0) {
229            return new String[0];
230        }
231        ArrayList<String> result = new ArrayList<>();
232        char[] chars = value.toCharArray();
233        StringBuilder sb = new StringBuilder();
234        boolean esc = false;
235        for (char c : chars) {
236            if (c == '\\') {
237                if (esc) {
238                    sb.append('\\');
239                    esc = false;
240                } else {
241                    esc = true;
242                }
243            } else if (c == ',') {
244                if (esc) {
245                    sb.append(',');
246                    esc = false;
247                } else {
248                    result.add(sb.toString());
249                    sb = new StringBuilder();
250                }
251            } else {
252                sb.append(c);
253            }
254        }
255        result.add(sb.toString());
256        return result.toArray(new String[result.size()]);
257    }
258
259    /**
260     * Sets the properties of a document based on their JSON representation (especially for scalar lists).
261     *
262     * @since 5.9.2
263     */
264    public static void setJSONProperties(CoreSession session, DocumentModel doc, Properties properties)
265            throws IOException {
266
267        for (Map.Entry<String, String> entry : properties.entrySet()) {
268            String key = entry.getKey();
269            String value = entry.getValue();
270            setProperty(session, doc, key, value, true);
271        }
272    }
273
274    /**
275     * @since 5.9.2
276     */
277    public static void setProperty(CoreSession session, DocumentModel doc, String key, String value,
278            boolean decodeStringListAsJSON) throws IOException {
279        if ("ecm:acl".equals(key)) {
280            setLocalAcl(session, doc, value);
281        }
282        Property p = doc.getProperty(key);
283        if (value == null || value.length() == 0) {
284            p.setValue(null);
285            return;
286        }
287        Type type = p.getField().getType();
288        if (!type.isSimpleType()) {
289            if (type.isListType()) {
290                ListType ltype = (ListType) type;
291                if (ltype.isScalarList() && !decodeStringListAsJSON) {
292                    p.setValue(readStringList(value, (SimpleType) ltype.getFieldType()));
293                    return;
294                } else {
295                    Object val = ComplexTypeJSONDecoder.decodeList(ltype, value);
296                    p.setValue(val);
297                    return;
298                }
299            } else if (type.isComplexType()) {
300                Object val = ComplexTypeJSONDecoder.decode((ComplexType) type, value);
301                p.setValue(val);
302                return;
303            }
304            throw new NuxeoException("Property type is not supported by this operation");
305        } else {
306            p.setValue(((SimpleType) type).getPrimitiveType().decode(value));
307        }
308    }
309
310}