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