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