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