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