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