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