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