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