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