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