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 */
018package org.nuxeo.ecm.core.opencmis.impl.server;
019
020import java.io.File;
021import java.io.FileOutputStream;
022import java.io.IOException;
023import java.io.InputStream;
024import java.io.OutputStream;
025import java.io.Serializable;
026import java.math.BigDecimal;
027import java.math.BigInteger;
028import java.util.ArrayList;
029import java.util.Arrays;
030import java.util.Collection;
031import java.util.Collections;
032import java.util.Enumeration;
033import java.util.GregorianCalendar;
034import java.util.Iterator;
035import java.util.List;
036import java.util.ListIterator;
037import java.util.Locale;
038
039import javax.servlet.http.HttpServletRequest;
040
041import org.apache.chemistry.opencmis.commons.PropertyIds;
042import org.apache.chemistry.opencmis.commons.data.ContentStream;
043import org.apache.chemistry.opencmis.commons.data.PropertyBoolean;
044import org.apache.chemistry.opencmis.commons.data.PropertyData;
045import org.apache.chemistry.opencmis.commons.data.PropertyDateTime;
046import org.apache.chemistry.opencmis.commons.data.PropertyDecimal;
047import org.apache.chemistry.opencmis.commons.data.PropertyHtml;
048import org.apache.chemistry.opencmis.commons.data.PropertyId;
049import org.apache.chemistry.opencmis.commons.data.PropertyInteger;
050import org.apache.chemistry.opencmis.commons.data.PropertyString;
051import org.apache.chemistry.opencmis.commons.data.PropertyUri;
052import org.apache.chemistry.opencmis.commons.definitions.PropertyDefinition;
053import org.apache.chemistry.opencmis.commons.enums.Cardinality;
054import org.apache.chemistry.opencmis.commons.enums.DateTimeFormat;
055import org.apache.chemistry.opencmis.commons.enums.PropertyType;
056import org.apache.chemistry.opencmis.commons.enums.Updatability;
057import org.apache.chemistry.opencmis.commons.exceptions.CmisContentAlreadyExistsException;
058import org.apache.chemistry.opencmis.commons.exceptions.CmisInvalidArgumentException;
059import org.apache.chemistry.opencmis.commons.exceptions.CmisRuntimeException;
060import org.apache.chemistry.opencmis.commons.exceptions.CmisStreamNotSupportedException;
061import org.apache.chemistry.opencmis.commons.impl.Constants;
062import org.apache.chemistry.opencmis.commons.impl.dataobjects.ContentStreamHashImpl;
063import org.apache.chemistry.opencmis.commons.server.CallContext;
064import org.apache.chemistry.opencmis.server.shared.HttpUtils;
065import org.apache.commons.codec.DecoderException;
066import org.apache.commons.codec.binary.Base64;
067import org.apache.commons.codec.binary.Hex;
068import org.apache.commons.io.IOUtils;
069import org.apache.commons.logging.Log;
070import org.apache.commons.logging.LogFactory;
071import org.nuxeo.ecm.automation.core.util.ComplexPropertyJSONEncoder;
072import org.nuxeo.ecm.automation.core.util.ComplexTypeJSONDecoder;
073import org.nuxeo.ecm.core.api.Blob;
074import org.nuxeo.ecm.core.api.Blobs;
075import org.nuxeo.ecm.core.api.CoreSession;
076import org.nuxeo.ecm.core.api.DocumentModel;
077import org.nuxeo.ecm.core.api.DocumentRef;
078import org.nuxeo.ecm.core.api.IdRef;
079import org.nuxeo.ecm.core.api.blobholder.BlobHolder;
080import org.nuxeo.ecm.core.api.model.Property;
081import org.nuxeo.ecm.core.api.model.PropertyNotFoundException;
082import org.nuxeo.ecm.core.api.model.impl.ComplexProperty;
083import org.nuxeo.ecm.core.io.download.DownloadService;
084import org.nuxeo.ecm.core.schema.DocumentType;
085import org.nuxeo.ecm.core.schema.FacetNames;
086import org.nuxeo.ecm.core.schema.SchemaManager;
087import org.nuxeo.ecm.core.schema.types.ComplexType;
088import org.nuxeo.ecm.core.schema.types.CompositeType;
089import org.nuxeo.ecm.core.schema.types.ListType;
090import org.nuxeo.ecm.core.schema.types.Type;
091import org.nuxeo.ecm.platform.filemanager.utils.FileManagerUtils;
092import org.nuxeo.runtime.api.Framework;
093
094import com.google.common.collect.Iterators;
095
096/**
097 * Nuxeo implementation of an object's property, backed by a property of a {@link DocumentModel}.
098 */
099public abstract class NuxeoPropertyData<T> extends NuxeoPropertyDataBase<T> {
100
101    private static final Log log = LogFactory.getLog(NuxeoPropertyData.class);
102
103    protected final String name;
104
105    protected final boolean readOnly;
106
107    protected final CallContext callContext;
108
109    public NuxeoPropertyData(PropertyDefinition<T> propertyDefinition, DocumentModel doc, String name,
110            boolean readOnly, CallContext callContext) {
111        super(propertyDefinition, doc);
112        this.name = name;
113        this.readOnly = readOnly;
114        this.callContext = callContext;
115    }
116
117    /**
118     * Factory for a new Property.
119     */
120    @SuppressWarnings("unchecked")
121    public static <U> PropertyData<U> construct(NuxeoObjectData data, PropertyDefinition<U> pd, CallContext callContext) {
122        DocumentModel doc = data.doc;
123        String name = pd.getId();
124        if (PropertyIds.OBJECT_ID.equals(name)) {
125            return (PropertyData<U>) new NuxeoPropertyIdDataFixed((PropertyDefinition<String>) pd, doc.getId());
126        } else if (PropertyIds.OBJECT_TYPE_ID.equals(name)) {
127            return (PropertyData<U>) new NuxeoPropertyIdDataFixed((PropertyDefinition<String>) pd,
128                    NuxeoTypeHelper.mappedId(doc.getType()));
129        } else if (PropertyIds.BASE_TYPE_ID.equals(name)) {
130            return (PropertyData<U>) new NuxeoPropertyIdDataFixed((PropertyDefinition<String>) pd,
131                    NuxeoTypeHelper.getBaseTypeId(doc).value());
132        } else if (PropertyIds.DESCRIPTION.equals(name)) {
133            return (PropertyData<U>) new NuxeoPropertyStringData((PropertyDefinition<String>) pd, doc,
134                    NuxeoTypeHelper.NX_DC_DESCRIPTION, false, callContext);
135        } else if (PropertyIds.CREATED_BY.equals(name)) {
136            return (PropertyData<U>) new NuxeoPropertyStringData((PropertyDefinition<String>) pd, doc,
137                    NuxeoTypeHelper.NX_DC_CREATOR, true, callContext);
138        } else if (PropertyIds.CREATION_DATE.equals(name)) {
139            return (PropertyData<U>) new NuxeoPropertyDateTimeData((PropertyDefinition<GregorianCalendar>) pd, doc,
140                    NuxeoTypeHelper.NX_DC_CREATED, true, callContext);
141        } else if (PropertyIds.LAST_MODIFIED_BY.equals(name)) {
142            return (PropertyData<U>) new NuxeoPropertyStringData((PropertyDefinition<String>) pd, doc,
143                    NuxeoTypeHelper.NX_DC_LAST_CONTRIBUTOR, true, callContext);
144        } else if (PropertyIds.LAST_MODIFICATION_DATE.equals(name)) {
145            return (PropertyData<U>) new NuxeoPropertyDateTimeData((PropertyDefinition<GregorianCalendar>) pd, doc,
146                    NuxeoTypeHelper.NX_DC_MODIFIED, true, callContext);
147        } else if (PropertyIds.CHANGE_TOKEN.equals(name)) {
148            return (PropertyData<U>) new NuxeoPropertyStringDataFixed((PropertyDefinition<String>) pd,
149                    doc.getChangeToken());
150        } else if (PropertyIds.NAME.equals(name)) {
151            return (PropertyData<U>) new NuxeoPropertyDataName((PropertyDefinition<String>) pd, doc);
152        } else if (PropertyIds.IS_IMMUTABLE.equals(name)) {
153            // TODO check write
154            return (PropertyData<U>) new NuxeoPropertyBooleanDataFixed((PropertyDefinition<Boolean>) pd, Boolean.FALSE);
155        } else if (PropertyIds.IS_LATEST_VERSION.equals(name)) {
156            return (PropertyData<U>) new NuxeoPropertyDataIsLatestVersion((PropertyDefinition<Boolean>) pd, doc);
157        } else if (PropertyIds.IS_LATEST_MAJOR_VERSION.equals(name)) {
158            return (PropertyData<U>) new NuxeoPropertyDataIsLatestMajorVersion((PropertyDefinition<Boolean>) pd, doc);
159        } else if (PropertyIds.IS_MAJOR_VERSION.equals(name)) {
160            return (PropertyData<U>) new NuxeoPropertyDataIsMajorVersion((PropertyDefinition<Boolean>) pd, doc);
161        } else if (PropertyIds.VERSION_LABEL.equals(name)) {
162            return (PropertyData<U>) new NuxeoPropertyDataVersionLabel((PropertyDefinition<String>) pd, doc);
163        } else if (PropertyIds.VERSION_SERIES_ID.equals(name)) {
164            // doesn't change once computed, no need to have a dynamic prop
165            String versionSeriesId = doc.getVersionSeriesId();
166            return (PropertyData<U>) new NuxeoPropertyIdDataFixed((PropertyDefinition<String>) pd, versionSeriesId);
167        } else if (PropertyIds.IS_VERSION_SERIES_CHECKED_OUT.equals(name)) {
168            return (PropertyData<U>) new NuxeoPropertyDataIsVersionSeriesCheckedOut((PropertyDefinition<Boolean>) pd,
169                    doc);
170        } else if (PropertyIds.VERSION_SERIES_CHECKED_OUT_BY.equals(name)) {
171            return (PropertyData<U>) new NuxeoPropertyDataVersionSeriesCheckedOutBy((PropertyDefinition<String>) pd,
172                    doc, callContext);
173        } else if (PropertyIds.VERSION_SERIES_CHECKED_OUT_ID.equals(name)) {
174            return (PropertyData<U>) new NuxeoPropertyDataVersionSeriesCheckedOutId((PropertyDefinition<String>) pd,
175                    doc);
176        } else if (NuxeoTypeHelper.NX_ISVERSION.equals(name)) {
177            return (PropertyData<U>) new NuxeoPropertyBooleanDataFixed((PropertyDefinition<Boolean>) pd,
178                    Boolean.valueOf(doc.isVersion()));
179        } else if (NuxeoTypeHelper.NX_ISCHECKEDIN.equals(name)) {
180            boolean co = doc.isCheckedOut();
181            return (PropertyData<U>) new NuxeoPropertyBooleanDataFixed((PropertyDefinition<Boolean>) pd,
182                    Boolean.valueOf(!co));
183        } else if (PropertyIds.IS_PRIVATE_WORKING_COPY.equals(name)) {
184            boolean co = doc.isCheckedOut();
185            return (PropertyData<U>) new NuxeoPropertyBooleanDataFixed((PropertyDefinition<Boolean>) pd,
186                    Boolean.valueOf(co));
187        } else if (PropertyIds.CHECKIN_COMMENT.equals(name)) {
188            return (PropertyData<U>) new NuxeoPropertyDataCheckInComment((PropertyDefinition<String>) pd, doc);
189        } else if (PropertyIds.CONTENT_STREAM_LENGTH.equals(name)) {
190            return (PropertyData<U>) new NuxeoPropertyDataContentStreamLength((PropertyDefinition<BigInteger>) pd, doc);
191        } else if (NuxeoTypeHelper.NX_DIGEST.equals(name)) {
192            return (PropertyData<U>) new NuxeoPropertyDataContentStreamDigest((PropertyDefinition<String>) pd, doc);
193        } else if (PropertyIds.CONTENT_STREAM_HASH.equals(name)) {
194            String digest = new NuxeoPropertyDataContentStreamDigest((PropertyDefinition<String>) pd, doc).getFirstValue();
195            List<String> hashes;
196            if (digest == null) {
197                hashes = new ArrayList<String>();
198            } else {
199                hashes = Arrays.asList(new ContentStreamHashImpl(ContentStreamHashImpl.ALGORITHM_MD5, digest).getPropertyValue());
200            }
201            return (PropertyData<U>) new NuxeoPropertyDataContentStreamHash((PropertyDefinition<String>) pd, hashes);
202        } else if (PropertyIds.CONTENT_STREAM_MIME_TYPE.equals(name)) {
203            return (PropertyData<U>) new NuxeoPropertyDataContentStreamMimeType((PropertyDefinition<String>) pd, doc);
204        } else if (PropertyIds.CONTENT_STREAM_FILE_NAME.equals(name)) {
205            return (PropertyData<U>) new NuxeoPropertyDataContentStreamFileName((PropertyDefinition<String>) pd, doc);
206        } else if (PropertyIds.CONTENT_STREAM_ID.equals(name)) {
207            return (PropertyData<U>) new NuxeoPropertyIdDataFixed((PropertyDefinition<String>) pd, null);
208        } else if (PropertyIds.PARENT_ID.equals(name)) {
209            return (PropertyData<U>) new NuxeoPropertyDataParentId((PropertyDefinition<String>) pd, doc);
210        } else if (NuxeoTypeHelper.NX_PARENT_ID.equals(name)) {
211            return (PropertyData<U>) new NuxeoPropertyDataParentId((PropertyDefinition<String>) pd, doc);
212        } else if (NuxeoTypeHelper.NX_PATH_SEGMENT.equals(name)) {
213            return (PropertyData<U>) new NuxeoPropertyStringDataFixed((PropertyDefinition<String>) pd, doc.getName());
214        } else if (NuxeoTypeHelper.NX_POS.equals(name)) {
215            return (PropertyData<U>) new NuxeoPropertyIntegerDataFixed((PropertyDefinition<BigInteger>) pd,
216                    doc.getPos());
217        } else if (PropertyIds.PATH.equals(name)) {
218            return (PropertyData<U>) new NuxeoPropertyDataPath((PropertyDefinition<String>) pd, doc);
219        } else if (PropertyIds.ALLOWED_CHILD_OBJECT_TYPE_IDS.equals(name)) {
220            return (PropertyData<U>) new NuxeoPropertyIdMultiDataFixed((PropertyDefinition<String>) pd,
221                    Collections.<String> emptyList());
222        } else if (PropertyIds.SOURCE_ID.equals(name)) {
223            return (PropertyData<U>) new NuxeoPropertyIdData((PropertyDefinition<String>) pd, doc,
224                    NuxeoTypeHelper.NX_REL_SOURCE, false, callContext);
225        } else if (PropertyIds.TARGET_ID.equals(name)) {
226            return (PropertyData<U>) new NuxeoPropertyIdData((PropertyDefinition<String>) pd, doc,
227                    NuxeoTypeHelper.NX_REL_TARGET, false, callContext);
228        } else if (PropertyIds.POLICY_TEXT.equals(name)) {
229            return (PropertyData<U>) new NuxeoPropertyStringDataFixed((PropertyDefinition<String>) pd, null);
230        } else if (PropertyIds.SECONDARY_OBJECT_TYPE_IDS.equals(name)) {
231            List<String> facets = getSecondaryTypeIds(doc);
232            return (PropertyData<U>) new NuxeoPropertyIdMultiDataFixed((PropertyDefinition<String>) pd, facets);
233        } else if (NuxeoTypeHelper.NX_FACETS.equals(name)) {
234            List<String> facets = getFacets(doc);
235            return (PropertyData<U>) new NuxeoPropertyIdMultiDataFixed((PropertyDefinition<String>) pd, facets);
236        } else if (NuxeoTypeHelper.NX_LIFECYCLE_STATE.equals(name)) {
237            String state = doc.getCurrentLifeCycleState();
238            return (PropertyData<U>) new NuxeoPropertyStringDataFixed((PropertyDefinition<String>) pd, state);
239        } else {
240            boolean readOnly = pd.getUpdatability() != Updatability.READWRITE;
241            // TODO WHEN_CHECKED_OUT, ON_CREATE
242
243            switch (pd.getPropertyType()) {
244            case BOOLEAN:
245                return (PropertyData<U>) new NuxeoPropertyBooleanData((PropertyDefinition<Boolean>) pd, doc, name,
246                        readOnly, callContext);
247            case DATETIME:
248                return (PropertyData<U>) new NuxeoPropertyDateTimeData((PropertyDefinition<GregorianCalendar>) pd, doc,
249                        name, readOnly, callContext);
250            case DECIMAL:
251                return (PropertyData<U>) new NuxeoPropertyDecimalData((PropertyDefinition<BigDecimal>) pd, doc, name,
252                        readOnly, callContext);
253            case HTML:
254                return (PropertyData<U>) new NuxeoPropertyHtmlData((PropertyDefinition<String>) pd, doc, name,
255                        readOnly, callContext);
256            case ID:
257                return (PropertyData<U>) new NuxeoPropertyIdData((PropertyDefinition<String>) pd, doc, name, readOnly,
258                        callContext);
259            case INTEGER:
260                return (PropertyData<U>) new NuxeoPropertyIntegerData((PropertyDefinition<BigInteger>) pd, doc, name,
261                        readOnly, callContext);
262            case STRING:
263                return (PropertyData<U>) new NuxeoPropertyStringData((PropertyDefinition<String>) pd, doc, name,
264                        readOnly, callContext);
265            case URI:
266                return (PropertyData<U>) new NuxeoPropertyUriData((PropertyDefinition<String>) pd, doc, name, readOnly,
267                        callContext);
268            default:
269                throw new AssertionError(pd.getPropertyType().toString());
270            }
271        }
272    }
273
274    /** Gets the doc's relevant facets. */
275    public static List<String> getFacets(DocumentModel doc) {
276        List<String> facets = new ArrayList<>();
277        SchemaManager schemaManager = Framework.getService(SchemaManager.class);
278        for (String facet : doc.getFacets()) {
279            // immutable facet is not actually stored or registered
280            if (!FacetNames.IMMUTABLE.equals(facet)) {
281                CompositeType facetType = schemaManager.getFacet(facet);
282                if (facetType != null) {
283                    facets.add(facet);
284                } else {
285                    log.warn("The document " + doc.getName() + " with id=" + doc.getId() + " and type=" + doc.getDocumentType().getName() + " contains the facet '" + facet
286                            + "', which is not registered as available in the schemaManager. This facet will be ignored.");
287                    if (log.isDebugEnabled()) {
288                        log.debug("Available facets: " + Arrays.toString(schemaManager.getFacets()));
289                    }
290                }
291            }
292        }
293        Collections.sort(facets);
294        return facets;
295    }
296
297    /** Gets the doc's secondary type ids. */
298    public static List<String> getSecondaryTypeIds(DocumentModel doc) {
299        List<String> facets = getFacets(doc);
300        DocumentType type = doc.getDocumentType();
301        for (ListIterator<String> it = facets.listIterator(); it.hasNext();) {
302            // remove those already in the doc type
303            String facet = it.next();
304            if (type.hasFacet(facet)) {
305                it.remove();
306                continue;
307            }
308            // add prefix
309            it.set(NuxeoTypeHelper.FACET_TYPE_PREFIX + facet);
310        }
311        return facets;
312    }
313
314    public static ContentStream getContentStream(DocumentModel doc, HttpServletRequest request)
315            throws CmisRuntimeException {
316        BlobHolder blobHolder = doc.getAdapter(BlobHolder.class);
317        if (blobHolder == null) {
318            throw new CmisStreamNotSupportedException();
319        }
320        Blob blob = blobHolder.getBlob();
321        if (blob == null) {
322            return null;
323        }
324        GregorianCalendar lastModified = (GregorianCalendar) doc.getPropertyValue("dc:modified");
325        return NuxeoContentStream.create(doc, DownloadService.BLOBHOLDER_0, blob, "cmis", null, lastModified, request);
326    }
327
328    public static void setContentStream(DocumentModel doc, ContentStream contentStream, boolean overwrite)
329            throws IOException, CmisContentAlreadyExistsException, CmisRuntimeException {
330        BlobHolder blobHolder = doc.getAdapter(BlobHolder.class);
331        if (blobHolder == null) {
332            throw new CmisContentAlreadyExistsException();
333        }
334        Blob oldBlob = blobHolder.getBlob();
335        if (!overwrite && oldBlob != null) {
336            throw new CmisContentAlreadyExistsException();
337        }
338        Blob blob;
339        if (contentStream == null) {
340            blob = null;
341        } else {
342            // default filename if none provided
343            String filename = contentStream.getFileName();
344            if (filename == null && oldBlob != null) {
345                filename = oldBlob.getFilename();
346            }
347            if (filename == null) {
348                filename = doc.getTitle();
349            }
350            blob = getPersistentBlob(contentStream, filename);
351        }
352        blobHolder.setBlob(blob);
353    }
354
355    /** Returns a Blob whose stream can be used several times. */
356    public static Blob getPersistentBlob(ContentStream contentStream, String filename) throws IOException {
357        if (filename == null) {
358            filename = contentStream.getFileName();
359        }
360        if (filename != null) {
361            // keep only the filename, no path
362            filename = FileManagerUtils.fetchFileName(filename);
363        }
364        File file = Framework.createTempFile("NuxeoCMIS-", null);
365        try (InputStream in = contentStream.getStream(); OutputStream out = new FileOutputStream(file)){
366            IOUtils.copy(in, out);
367            Framework.trackFile(file, in);
368        }
369        return Blobs.createBlob(file, contentStream.getMimeType(), null, filename);
370    }
371
372    public static void validateBlobDigest(DocumentModel doc, CallContext callContext) {
373        Blob blob = doc.getAdapter(BlobHolder.class).getBlob();
374        if (blob == null) {
375            return;
376        }
377        String blobDigestAlgorithm = blob.getDigestAlgorithm();
378        if (blobDigestAlgorithm == null) {
379            return;
380        }
381        HttpServletRequest request = (HttpServletRequest) callContext.get(CallContext.HTTP_SERVLET_REQUEST);
382        String reqDigest = NuxeoPropertyData.extractDigestFromRequestHeaders(request, blobDigestAlgorithm);
383        if (reqDigest == null) {
384            return;
385        }
386        String blobDigest = blob.getDigest();
387        if (!blobDigest.equals(reqDigest)) {
388            throw new CmisInvalidArgumentException(String.format(
389                    "Content Stream Hex-encoded Digest: '%s' must equal Request Header Hex-encoded Digest: '%s'",
390                    blobDigest, reqDigest));
391        }
392    }
393
394    protected static String extractDigestFromRequestHeaders(HttpServletRequest request, String digestAlgorithm) {
395        if (request == null) {
396            return null;
397        }
398        Enumeration<String> digests = request.getHeaders(NuxeoContentStream.DIGEST_HEADER_NAME);
399        if (digests == null) {
400            return null;
401        }
402        Iterator<String> it = Iterators.forEnumeration(digests);
403        while (it.hasNext()) {
404            String value = it.next();
405            int equals = value.indexOf('=');
406            if (equals < 0) {
407                continue;
408            }
409            String reqDigestAlgorithm = value.substring(0, equals);
410            if (reqDigestAlgorithm.equalsIgnoreCase(digestAlgorithm)) {
411                String digest = value.substring(equals + 1);
412                digest = transcodeBase64ToHex(digest);
413                return digest;
414            }
415        }
416        return null;
417    }
418
419    public static String transcodeBase64ToHex(String base64String){
420        byte[] bytes = Base64.decodeBase64(base64String);
421        String hexString = Hex.encodeHexString(bytes);
422        return hexString;
423    }
424
425    public static String transcodeHexToBase64(String hexString) {
426        byte[] bytes;
427        try {
428            bytes = Hex.decodeHex(hexString.toCharArray());
429        } catch (DecoderException e) {
430            throw new RuntimeException(e);
431        }
432        String base64String = Base64.encodeBase64String(bytes);
433        return base64String;
434    }
435
436    /**
437     * Conversion from Nuxeo values to CMIS ones.
438     *
439     * @return either a primitive type or a List of them, or {@code null}
440     */
441    @Override
442    @SuppressWarnings("unchecked")
443    public <U> U getValue() {
444        Property prop = doc.getProperty(name);
445        Serializable value = prop.getValue();
446        if (value == null) {
447            return null;
448        }
449        Type type = prop.getType();
450        if (type.isListType()) {
451            // array/list
452            type = ((ListType) type).getFieldType();
453            Collection<Object> values;
454            if (type.isComplexType()) {
455                values = (Collection) prop.getChildren();
456            } else if (value instanceof Object[]) {
457                values = Arrays.asList((Object[]) value);
458            } else if (value instanceof List<?>) {
459                values = (List<Object>) value;
460            } else {
461                throw new CmisRuntimeException("Unknown value type: " + value.getClass().getName());
462            }
463            List<Object> list = new ArrayList<>(values);
464            for (int i = 0; i < list.size(); i++) {
465                if (type.isComplexType()) {
466                    value = (Serializable) convertComplexPropertyToCMIS((ComplexProperty) list.get(i), callContext);
467                } else {
468                    value = (Serializable) convertToCMIS(list.get(i));
469                }
470                list.set(i, value);
471            }
472            return (U) list;
473        } else {
474            // primitive type or complex type
475            if (type.isComplexType()) {
476                value = (Serializable) convertComplexPropertyToCMIS((ComplexProperty) prop, callContext);
477            } else {
478                value = (Serializable) convertToCMIS(value);
479            }
480            return (U) convertToCMIS(value);
481        }
482    }
483
484    // conversion from Nuxeo value types to CMIS ones
485    protected static Object convertToCMIS(Object value) {
486        if (value instanceof Double) {
487            return BigDecimal.valueOf(((Double) value).doubleValue());
488        } else if (value instanceof Integer) {
489            return BigInteger.valueOf(((Integer) value).intValue());
490        } else if (value instanceof Long) {
491            return BigInteger.valueOf(((Long) value).longValue());
492        } else {
493            return value;
494        }
495    }
496
497    protected static Object convertComplexPropertyToCMIS(ComplexProperty prop, CallContext callContext) {
498        DateTimeFormat cmisDateTimeFormat = getCMISDateTimeFormat(callContext);
499        org.nuxeo.ecm.automation.core.util.DateTimeFormat nuxeoDateTimeFormat = cmisDateTimeFormat == DateTimeFormat.SIMPLE
500                ? org.nuxeo.ecm.automation.core.util.DateTimeFormat.TIME_IN_MILLIS
501                : org.nuxeo.ecm.automation.core.util.DateTimeFormat.W3C;
502        try {
503            return ComplexPropertyJSONEncoder.encode(prop, nuxeoDateTimeFormat);
504        } catch (IOException e) {
505            throw new CmisRuntimeException(e.toString(), e);
506        }
507    }
508
509    protected static DateTimeFormat getCMISDateTimeFormat(CallContext callContext) {
510        if (callContext != null && CallContext.BINDING_BROWSER.equals(callContext.getBinding())) {
511            HttpServletRequest request = (HttpServletRequest) callContext.get(CallContext.HTTP_SERVLET_REQUEST);
512            if (request != null) {
513                String s = HttpUtils.getStringParameter(request, Constants.PARAM_DATETIME_FORMAT);
514                if (s != null) {
515                    try {
516                        return DateTimeFormat.fromValue(s.trim().toLowerCase(Locale.ENGLISH));
517                    } catch (IllegalArgumentException e) {
518                        throw new CmisInvalidArgumentException("Invalid value for parameter "
519                                + Constants.PARAM_DATETIME_FORMAT + "!");
520                    }
521                }
522            }
523            return DateTimeFormat.SIMPLE;
524        }
525        return DateTimeFormat.EXTENDED;
526    }
527
528    // conversion from CMIS value types to Nuxeo ones
529    protected static Object convertToNuxeo(Object value, Type type) {
530        if (value instanceof BigDecimal) {
531            return Double.valueOf(((BigDecimal) value).doubleValue());
532        } else if (value instanceof BigInteger) {
533            return Long.valueOf(((BigInteger) value).longValue());
534        } else if (type.isComplexType()) {
535            try {
536                return ComplexTypeJSONDecoder.decode((ComplexType) type, value.toString());
537            } catch (IOException e) {
538                throw new CmisRuntimeException(e.toString(), e);
539            }
540        } else {
541            return value;
542        }
543    }
544
545    /**
546     * Validates a CMIS value according to a property definition.
547     */
548    @SuppressWarnings("unchecked")
549    public static <T> void validateCMISValue(Object value, PropertyDefinition<T> pd) {
550        if (value == null) {
551            return;
552        }
553        List<T> values;
554        if (value instanceof List<?>) {
555            if (pd.getCardinality() != Cardinality.MULTI) {
556                throw new CmisInvalidArgumentException("Property is single-valued: " + pd.getId());
557            }
558            values = (List<T>) value;
559            if (values.isEmpty()) {
560                return;
561            }
562        } else {
563            if (pd.getCardinality() != Cardinality.SINGLE) {
564                throw new CmisInvalidArgumentException("Property is multi-valued: " + pd.getId());
565            }
566            values = Collections.singletonList((T) value);
567        }
568        PropertyType type = pd.getPropertyType();
569        for (Object v : values) {
570            if (v == null) {
571                throw new CmisInvalidArgumentException("Null values not allowed: " + values);
572            }
573            boolean ok;
574            switch (type) {
575            case STRING:
576            case ID:
577            case URI:
578            case HTML:
579                ok = v instanceof String;
580                break;
581            case INTEGER:
582                ok = v instanceof BigInteger || v instanceof Byte || v instanceof Short || v instanceof Integer
583                        || v instanceof Long;
584                break;
585            case DECIMAL:
586                ok = v instanceof BigDecimal;
587                break;
588            case BOOLEAN:
589                ok = v instanceof Boolean;
590                break;
591            case DATETIME:
592                ok = v instanceof GregorianCalendar;
593                break;
594            default:
595                throw new RuntimeException(type.toString());
596            }
597            if (!ok) {
598                throw new CmisInvalidArgumentException("Value does not match property type " + type + ":  " + v);
599            }
600        }
601    }
602
603    @SuppressWarnings("unchecked")
604    @Override
605    public T getFirstValue() {
606        Object value = getValue();
607        if (value == null) {
608            return null;
609        }
610        if (value instanceof List) {
611            List<?> list = (List<?>) value;
612            if (list.isEmpty()) {
613                return null;
614            }
615            return (T) list.get(0);
616        } else {
617            return (T) value;
618        }
619    }
620
621    @SuppressWarnings("unchecked")
622    @Override
623    public List<T> getValues() {
624        Object value = getValue();
625        if (value == null) {
626            return Collections.emptyList();
627        }
628        if (value instanceof List) {
629            return (List<T>) value;
630        } else {
631            return (List<T>) Collections.singletonList(value);
632        }
633    }
634
635    @Override
636    public void setValue(Object value) {
637        if (readOnly) {
638            super.setValue(value);
639        } else {
640            Type type = doc.getProperty(name).getType();
641            if (type.isListType()) {
642                type = ((ListType) type).getFieldType();
643            }
644            Object propValue;
645            if (value instanceof List<?>) {
646                @SuppressWarnings("unchecked")
647                List<Object> list = new ArrayList<>((List<Object>) value);
648                for (int i = 0; i < list.size(); i++) {
649                    list.set(i, convertToNuxeo(list.get(i), type));
650                }
651                if (list.isEmpty()) {
652                    list = null;
653                }
654                propValue = list;
655            } else {
656                propValue = convertToNuxeo(value, type);
657            }
658            doc.setPropertyValue(name, (Serializable) propValue);
659        }
660    }
661
662    protected static Blob getBlob(DocumentModel doc) throws CmisRuntimeException {
663        BlobHolder blobHolder = doc.getAdapter(BlobHolder.class);
664        if (blobHolder == null) {
665            return null;
666        }
667        return blobHolder.getBlob();
668    }
669
670    public static class NuxeoPropertyStringData extends NuxeoPropertyData<String> implements PropertyString {
671        public NuxeoPropertyStringData(PropertyDefinition<String> propertyDefinition, DocumentModel doc, String name,
672                boolean readOnly, CallContext callContext) {
673            super(propertyDefinition, doc, name, readOnly, callContext);
674        }
675    }
676
677    public static class NuxeoPropertyIdData extends NuxeoPropertyData<String> implements PropertyId {
678        public NuxeoPropertyIdData(PropertyDefinition<String> propertyDefinition, DocumentModel doc, String name,
679                boolean readOnly, CallContext callContext) {
680            super(propertyDefinition, doc, name, readOnly, callContext);
681        }
682    }
683
684    public static class NuxeoPropertyBooleanData extends NuxeoPropertyData<Boolean> implements PropertyBoolean {
685        public NuxeoPropertyBooleanData(PropertyDefinition<Boolean> propertyDefinition, DocumentModel doc, String name,
686                boolean readOnly, CallContext callContext) {
687            super(propertyDefinition, doc, name, readOnly, callContext);
688        }
689    }
690
691    public static class NuxeoPropertyIntegerData extends NuxeoPropertyData<BigInteger> implements PropertyInteger {
692        public NuxeoPropertyIntegerData(PropertyDefinition<BigInteger> propertyDefinition, DocumentModel doc,
693                String name, boolean readOnly, CallContext callContext) {
694            super(propertyDefinition, doc, name, readOnly, callContext);
695        }
696    }
697
698    public static class NuxeoPropertyDecimalData extends NuxeoPropertyData<BigDecimal> implements PropertyDecimal {
699        public NuxeoPropertyDecimalData(PropertyDefinition<BigDecimal> propertyDefinition, DocumentModel doc,
700                String name, boolean readOnly, CallContext callContext) {
701            super(propertyDefinition, doc, name, readOnly, callContext);
702        }
703    }
704
705    public static class NuxeoPropertyDateTimeData extends NuxeoPropertyData<GregorianCalendar> implements
706            PropertyDateTime {
707        public NuxeoPropertyDateTimeData(PropertyDefinition<GregorianCalendar> propertyDefinition, DocumentModel doc,
708                String name, boolean readOnly, CallContext callContext) {
709            super(propertyDefinition, doc, name, readOnly, callContext);
710        }
711    }
712
713    public static class NuxeoPropertyHtmlData extends NuxeoPropertyData<String> implements PropertyHtml {
714        public NuxeoPropertyHtmlData(PropertyDefinition<String> propertyDefinition, DocumentModel doc, String name,
715                boolean readOnly, CallContext callContext) {
716            super(propertyDefinition, doc, name, readOnly, callContext);
717        }
718    }
719
720    public static class NuxeoPropertyUriData extends NuxeoPropertyData<String> implements PropertyUri {
721        public NuxeoPropertyUriData(PropertyDefinition<String> propertyDefinition, DocumentModel doc, String name,
722                boolean readOnly, CallContext callContext) {
723            super(propertyDefinition, doc, name, readOnly, callContext);
724        }
725    }
726
727    /**
728     * Property for cmis:contentStreamFileName.
729     */
730    public static class NuxeoPropertyDataContentStreamFileName extends NuxeoPropertyDataBase<String> implements
731            PropertyString {
732
733        protected NuxeoPropertyDataContentStreamFileName(PropertyDefinition<String> propertyDefinition,
734                DocumentModel doc) {
735            super(propertyDefinition, doc);
736        }
737
738        @Override
739        public String getFirstValue() {
740            Blob blob = getBlob(doc);
741            return blob == null ? null : blob.getFilename();
742        }
743
744        // @Override
745        // public void setValue(Serializable value) {
746        // BlobHolder blobHolder = docHolder.getDocumentModel().getAdapter(
747        // BlobHolder.class);
748        // if (blobHolder == null) {
749        // throw new StreamNotSupportedException();
750        // }
751        // Blob blob;
752        // blob = blobHolder.getBlob();
753        // if (blob != null) {
754        // blob.setFilename((String) value);
755        // }
756        // }
757    }
758
759    /**
760     * Property for cmis:contentStreamLength.
761     */
762    public static class NuxeoPropertyDataContentStreamLength extends NuxeoPropertyDataBase<BigInteger> implements
763            PropertyInteger {
764
765        protected NuxeoPropertyDataContentStreamLength(PropertyDefinition<BigInteger> propertyDefinition,
766                DocumentModel doc) {
767            super(propertyDefinition, doc);
768        }
769
770        @Override
771        public BigInteger getFirstValue() {
772            Blob blob = getBlob(doc);
773            return blob == null ? null : BigInteger.valueOf(blob.getLength());
774        }
775    }
776
777    /**
778     * Property for nuxeo:contentStreamDigest.
779     */
780    public static class NuxeoPropertyDataContentStreamDigest extends NuxeoPropertyDataBase<String> implements
781            PropertyString {
782
783        protected NuxeoPropertyDataContentStreamDigest(PropertyDefinition<String> propertyDefinition, DocumentModel doc) {
784            super(propertyDefinition, doc);
785        }
786
787        @Override
788        public String getFirstValue() {
789            Blob blob = getBlob(doc);
790            return blob == null ? null : blob.getDigest();
791        }
792    }
793
794    /**
795     * Property for cmis:contentStreamHash.
796     */
797    public static class NuxeoPropertyDataContentStreamHash extends NuxeoPropertyMultiDataFixed<String> implements
798            PropertyString {
799
800        protected NuxeoPropertyDataContentStreamHash(PropertyDefinition<String> propertyDefinition, List<String> hashes) {
801            super(propertyDefinition, hashes);
802        }
803    }
804
805    /**
806     * Property for cmis:contentMimeTypeLength.
807     */
808    public static class NuxeoPropertyDataContentStreamMimeType extends NuxeoPropertyDataBase<String> implements
809            PropertyString {
810
811        protected NuxeoPropertyDataContentStreamMimeType(PropertyDefinition<String> propertyDefinition,
812                DocumentModel doc) {
813            super(propertyDefinition, doc);
814        }
815
816        @Override
817        public String getFirstValue() {
818            Blob blob = getBlob(doc);
819            return blob == null ? null : blob.getMimeType();
820        }
821    }
822
823    /**
824     * Property for cmis:name.
825     */
826    public static class NuxeoPropertyDataName extends NuxeoPropertyDataBase<String> implements PropertyString {
827
828        private static final Log log = LogFactory.getLog(NuxeoPropertyDataName.class);
829
830        protected NuxeoPropertyDataName(PropertyDefinition<String> propertyDefinition, DocumentModel doc) {
831            super(propertyDefinition, doc);
832        }
833
834        /**
835         * Gets the value for the cmis:name property.
836         */
837        public static String getValue(DocumentModel doc) {
838            if (doc.getPath() == null) {
839                // not a real doc (content changes)
840                return "";
841            }
842            if (doc.getPath().isRoot()) {
843                return ""; // Nuxeo root
844            }
845            return doc.getTitle();
846        }
847
848        @Override
849        public String getFirstValue() {
850            return getValue(doc);
851        }
852
853        @Override
854        public void setValue(Object value) {
855            try {
856                doc.setPropertyValue(NuxeoTypeHelper.NX_DC_TITLE, (String) value);
857            } catch (PropertyNotFoundException e) {
858                // trying to set the name of a type with no dublincore
859                // ignore
860                log.debug("Cannot set CMIS name on type: " + doc.getType());
861            }
862        }
863    }
864
865    /**
866     * Property for cmis:parentId and nuxeo:parentId.
867     */
868    public static class NuxeoPropertyDataParentId extends NuxeoPropertyDataBase<String> implements PropertyId {
869
870        protected NuxeoPropertyDataParentId(PropertyDefinition<String> propertyDefinition, DocumentModel doc) {
871            super(propertyDefinition, doc);
872        }
873
874        @Override
875        public String getFirstValue() {
876            if (doc.getName() == null) {
877                return null;
878            } else {
879                DocumentRef parentRef = doc.getParentRef();
880                if (parentRef == null) {
881                    return null; // unfiled document
882                } else if (parentRef instanceof IdRef) {
883                    return ((IdRef) parentRef).value;
884                } else {
885                    return doc.getCoreSession().getDocument(parentRef).getId();
886                }
887            }
888        }
889    }
890
891    /**
892     * Property for cmis:path.
893     */
894    public static class NuxeoPropertyDataPath extends NuxeoPropertyDataBase<String> implements PropertyString {
895
896        protected NuxeoPropertyDataPath(PropertyDefinition<String> propertyDefinition, DocumentModel doc) {
897            super(propertyDefinition, doc);
898        }
899
900        @Override
901        public String getFirstValue() {
902            String path = doc.getPathAsString();
903            return path == null ? "" : path;
904        }
905    }
906
907    protected static boolean isVersionOrProxyToVersion(DocumentModel doc) {
908        return doc.isVersion() || (doc.isProxy() && doc.getCoreSession().getSourceDocument(doc.getRef()).isVersion());
909    }
910
911    /**
912     * Property for cmis:isMajorVersion.
913     */
914    public static class NuxeoPropertyDataIsMajorVersion extends NuxeoPropertyDataBase<Boolean> implements
915            PropertyBoolean {
916
917        protected NuxeoPropertyDataIsMajorVersion(PropertyDefinition<Boolean> propertyDefinition, DocumentModel doc) {
918            super(propertyDefinition, doc);
919        }
920
921        @Override
922        public Boolean getFirstValue() {
923            if (isVersionOrProxyToVersion(doc)) {
924                return Boolean.valueOf(doc.isMajorVersion());
925            }
926            // checked in doc considered latest version
927            return Boolean.valueOf(!doc.isCheckedOut() && doc.getVersionLabel().endsWith(".0"));
928        }
929    }
930
931    /**
932     * Property for cmis:isLatestVersion.
933     */
934    public static class NuxeoPropertyDataIsLatestVersion extends NuxeoPropertyDataBase<Boolean> implements
935            PropertyBoolean {
936
937        protected NuxeoPropertyDataIsLatestVersion(PropertyDefinition<Boolean> propertyDefinition, DocumentModel doc) {
938            super(propertyDefinition, doc);
939        }
940
941        @Override
942        public Boolean getFirstValue() {
943            if (isVersionOrProxyToVersion(doc)) {
944                return Boolean.valueOf(doc.isLatestVersion());
945            }
946            // checked in doc considered latest version
947            return Boolean.valueOf(!doc.isCheckedOut());
948        }
949    }
950
951    /**
952     * Property for cmis:isLatestMajorVersion.
953     */
954    public static class NuxeoPropertyDataIsLatestMajorVersion extends NuxeoPropertyDataBase<Boolean> implements
955            PropertyBoolean {
956
957        protected NuxeoPropertyDataIsLatestMajorVersion(PropertyDefinition<Boolean> propertyDefinition,
958                DocumentModel doc) {
959            super(propertyDefinition, doc);
960        }
961
962        @Override
963        public Boolean getFirstValue() {
964            if (isVersionOrProxyToVersion(doc)) {
965                return Boolean.valueOf(doc.isLatestMajorVersion());
966            }
967            // checked in doc considered latest version
968            return Boolean.valueOf(!doc.isCheckedOut() && doc.getVersionLabel().endsWith(".0"));
969        }
970    }
971
972    /**
973     * Property for cmis:isVersionSeriesCheckedOut.
974     */
975    public static class NuxeoPropertyDataIsVersionSeriesCheckedOut extends NuxeoPropertyDataBase<Boolean> implements
976            PropertyBoolean {
977
978        protected NuxeoPropertyDataIsVersionSeriesCheckedOut(PropertyDefinition<Boolean> propertyDefinition,
979                DocumentModel doc) {
980            super(propertyDefinition, doc);
981        }
982
983        @Override
984        public Boolean getFirstValue() {
985            return Boolean.valueOf(doc.isVersionSeriesCheckedOut());
986        }
987    }
988
989    /**
990     * Property for cmis:versionSeriesCheckedOutId.
991     */
992    public static class NuxeoPropertyDataVersionSeriesCheckedOutId extends NuxeoPropertyDataBase<String> implements
993            PropertyId {
994
995        protected NuxeoPropertyDataVersionSeriesCheckedOutId(PropertyDefinition<String> propertyDefinition,
996                DocumentModel doc) {
997            super(propertyDefinition, doc);
998        }
999
1000        @Override
1001        public String getFirstValue() {
1002            if (!doc.isVersionSeriesCheckedOut()) {
1003                return null;
1004            }
1005            return doc.getCoreSession().getVersionSeriesId(doc.getRef());
1006        }
1007    }
1008
1009    /**
1010     * Property for cmis:versionSeriesCheckedOutBy.
1011     */
1012    public static class NuxeoPropertyDataVersionSeriesCheckedOutBy extends NuxeoPropertyDataBase<String> implements
1013            PropertyString {
1014
1015        protected final CallContext callContext;
1016
1017        protected NuxeoPropertyDataVersionSeriesCheckedOutBy(PropertyDefinition<String> propertyDefinition,
1018                DocumentModel doc, CallContext callContext) {
1019            super(propertyDefinition, doc);
1020            this.callContext = callContext;
1021        }
1022
1023        @Override
1024        public String getFirstValue() {
1025            if (!doc.isVersionSeriesCheckedOut()) {
1026                return null;
1027            }
1028            String versionSeriesId = doc.getCoreSession().getVersionSeriesId(doc.getRef());
1029            // TODO not implemented
1030            return versionSeriesId == null ? null : callContext.getUsername();
1031        }
1032    }
1033
1034    /**
1035     * Property for cmis:versionLabel.
1036     */
1037    public static class NuxeoPropertyDataVersionLabel extends NuxeoPropertyDataBase<String> implements PropertyString {
1038
1039        protected NuxeoPropertyDataVersionLabel(PropertyDefinition<String> propertyDefinition, DocumentModel doc) {
1040            super(propertyDefinition, doc);
1041        }
1042
1043        @Override
1044        public String getFirstValue() {
1045            if (isVersionOrProxyToVersion(doc)) {
1046                return doc.getVersionLabel();
1047            }
1048            return doc.isCheckedOut() ? null : doc.getVersionLabel();
1049        }
1050    }
1051
1052    /**
1053     * Property for cmis:checkinComment.
1054     */
1055    public static class NuxeoPropertyDataCheckInComment extends NuxeoPropertyDataBase<String> implements PropertyString {
1056
1057        protected NuxeoPropertyDataCheckInComment(PropertyDefinition<String> propertyDefinition, DocumentModel doc) {
1058            super(propertyDefinition, doc);
1059        }
1060
1061        @Override
1062        public String getFirstValue() {
1063            if (isVersionOrProxyToVersion(doc)) {
1064                return doc.getCheckinComment();
1065            }
1066            if (doc.isCheckedOut()) {
1067                return null;
1068            }
1069            CoreSession session = doc.getCoreSession();
1070            DocumentRef v = session.getBaseVersion(doc.getRef());
1071            DocumentModel ver = session.getDocument(v);
1072            return ver.getCheckinComment();
1073        }
1074    }
1075
1076}