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