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