001/* 002 * (C) Copyright 2006-2017 Nuxeo (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 * Florent Guillaume 018 * Laurent Doguin 019 */ 020package org.nuxeo.ecm.core.versioning; 021 022import static org.nuxeo.ecm.core.api.VersioningOption.MAJOR; 023import static org.nuxeo.ecm.core.api.VersioningOption.MINOR; 024import static org.nuxeo.ecm.core.api.VersioningOption.NONE; 025 026import java.io.Serializable; 027import java.util.ArrayList; 028import java.util.Arrays; 029import java.util.Collections; 030import java.util.HashMap; 031import java.util.List; 032import java.util.Map; 033import java.util.Map.Entry; 034import java.util.function.Function; 035import java.util.stream.Collectors; 036 037import org.apache.commons.logging.Log; 038import org.apache.commons.logging.LogFactory; 039import org.nuxeo.ecm.core.api.DocumentModel; 040import org.nuxeo.ecm.core.api.DocumentModelFactory; 041import org.nuxeo.ecm.core.api.LifeCycleException; 042import org.nuxeo.ecm.core.api.NuxeoException; 043import org.nuxeo.ecm.core.api.VersioningOption; 044import org.nuxeo.ecm.core.api.impl.DocumentModelImpl; 045import org.nuxeo.ecm.core.api.model.PropertyNotFoundException; 046import org.nuxeo.ecm.core.model.Document; 047import org.nuxeo.ecm.core.schema.FacetNames; 048 049/** 050 * Implementation of the versioning service that follows standard checkout / checkin semantics. 051 */ 052public class StandardVersioningService implements ExtendableVersioningService { 053 054 private static final Log log = LogFactory.getLog(StandardVersioningService.class); 055 056 protected static final int DEFAULT_FORMER_RULE_ORDER = 10_000; 057 058 protected static final String COMPAT_ID_PREFIX = "compatibility-type-"; 059 060 protected static final String COMPAT_DEFAULT_ID = "compatibility-default"; 061 062 /** 063 * @deprecated since 9.1 seems unused 064 */ 065 @Deprecated 066 public static final String FILE_TYPE = "File"; 067 068 /** 069 * @deprecated since 9.1 seems unused 070 */ 071 @Deprecated 072 public static final String NOTE_TYPE = "Note"; 073 074 /** 075 * @deprecated since 9.1 seems unused 076 */ 077 @Deprecated 078 public static final String PROJECT_STATE = "project"; 079 080 public static final String APPROVED_STATE = "approved"; 081 082 public static final String OBSOLETE_STATE = "obsolete"; 083 084 public static final String BACK_TO_PROJECT_TRANSITION = "backToProject"; 085 086 /** 087 * @deprecated since 9.1 seems unused 088 */ 089 @Deprecated 090 protected static final String AUTO_CHECKED_OUT = "AUTO_CHECKED_OUT"; 091 092 /** Key for major version in Document API. */ 093 protected static final String MAJOR_VERSION = "ecm:majorVersion"; 094 095 /** Key for minor version in Document API. */ 096 protected static final String MINOR_VERSION = "ecm:minorVersion"; 097 098 private Map<String, VersioningPolicyDescriptor> versioningPolicies = new HashMap<>(); 099 100 private Map<String, VersioningFilterDescriptor> versioningFilters = new HashMap<>(); 101 102 private Map<String, VersioningRestrictionDescriptor> versioningRestrictions = new HashMap<>(); 103 104 @Override 105 public String getVersionLabel(DocumentModel docModel) { 106 String label; 107 try { 108 label = getMajor(docModel) + "." + getMinor(docModel); 109 if (docModel.isCheckedOut() && !"0.0".equals(label)) { 110 label += "+"; 111 } 112 } catch (PropertyNotFoundException e) { 113 label = ""; 114 } 115 return label; 116 } 117 118 protected long getMajor(DocumentModel docModel) { 119 return getVersion(docModel, VersioningService.MAJOR_VERSION_PROP); 120 } 121 122 protected long getMinor(DocumentModel docModel) { 123 return getVersion(docModel, VersioningService.MINOR_VERSION_PROP); 124 } 125 126 protected long getVersion(DocumentModel docModel, String prop) { 127 Object propVal = docModel.getPropertyValue(prop); 128 if (propVal == null || !(propVal instanceof Long)) { 129 return 0; 130 } else { 131 return ((Long) propVal).longValue(); 132 } 133 } 134 135 protected long getMajor(Document doc) { 136 return getVersion(doc, MAJOR_VERSION); 137 } 138 139 protected long getMinor(Document doc) { 140 return getVersion(doc, MINOR_VERSION); 141 } 142 143 protected long getVersion(Document doc, String prop) { 144 Object propVal = doc.getPropertyValue(prop); 145 if (propVal == null || !(propVal instanceof Long)) { 146 return 0; 147 } else { 148 return ((Long) propVal).longValue(); 149 } 150 } 151 152 protected void setVersion(Document doc, long major, long minor) { 153 doc.setPropertyValue(MAJOR_VERSION, Long.valueOf(major)); 154 doc.setPropertyValue(MINOR_VERSION, Long.valueOf(minor)); 155 } 156 157 protected void incrementMajor(Document doc) { 158 setVersion(doc, getMajor(doc) + 1, 0); 159 } 160 161 protected void incrementMinor(Document doc) { 162 // make sure major is not null by re-setting it 163 setVersion(doc, getMajor(doc), getMinor(doc) + 1); 164 } 165 166 protected void incrementByOption(Document doc, VersioningOption option) { 167 try { 168 if (option == MAJOR) { 169 incrementMajor(doc); 170 } else if (option == MINOR) { 171 incrementMinor(doc); 172 } 173 // else nothing 174 } catch (PropertyNotFoundException e) { 175 // ignore 176 } 177 } 178 179 @Override 180 public void doPostCreate(Document doc, Map<String, Serializable> options) { 181 if (doc.isVersion() || doc.isProxy()) { 182 return; 183 } 184 setInitialVersion(doc); 185 } 186 187 /** 188 * Sets the initial version on a document. Can be overridden. 189 */ 190 protected void setInitialVersion(Document doc) { 191 // Create a document model for filters 192 DocumentModelImpl documentModel = DocumentModelFactory.createDocumentModel(doc, null, null); 193 for (VersioningPolicyDescriptor policyDescriptor : versioningPolicies.values()) { 194 if (isPolicyMatch(policyDescriptor, null, documentModel)) { 195 InitialStateDescriptor initialState = policyDescriptor.getInitialState(); 196 if (initialState != null) { 197 setVersion(doc, initialState.getMajor(), initialState.getMinor()); 198 return; 199 } 200 } 201 } 202 setVersion(doc, 0, 0); 203 } 204 205 @Override 206 public List<VersioningOption> getSaveOptions(DocumentModel docModel) { 207 boolean versionable = docModel.isVersionable(); 208 String lifeCycleState = docModel.getCoreSession().getCurrentLifeCycleState(docModel.getRef()); 209 String type = docModel.getType(); 210 return getSaveOptions(versionable, lifeCycleState, type); 211 } 212 213 protected List<VersioningOption> getSaveOptions(Document doc) { 214 boolean versionable = doc.getType().getFacets().contains(FacetNames.VERSIONABLE); 215 String lifeCycleState; 216 try { 217 lifeCycleState = doc.getLifeCycleState(); 218 } catch (LifeCycleException e) { 219 lifeCycleState = null; 220 } 221 String type = doc.getType().getName(); 222 return getSaveOptions(versionable, lifeCycleState, type); 223 } 224 225 protected List<VersioningOption> getSaveOptions(boolean versionable, String lifeCycleState, String type) { 226 if (!versionable) { 227 return Collections.singletonList(NONE); 228 } 229 230 // try to get restriction for current type 231 List<VersioningOption> options = computeRestrictionOptions(lifeCycleState, type); 232 if (options == null) { 233 // no specific restrictions on current document type - get restriction for any document type 234 options = computeRestrictionOptions(lifeCycleState, "*"); 235 } 236 if (options != null) { 237 return options; 238 } 239 240 // By default a versionable document could be incremented by all available options 241 return Arrays.asList(VersioningOption.values()); 242 } 243 244 protected List<VersioningOption> computeRestrictionOptions(String lifeCycleState, String type) { 245 VersioningRestrictionDescriptor restrictions = versioningRestrictions.get(type); 246 if (restrictions != null) { 247 // try to get restriction options for current life cycle state 248 VersioningRestrictionOptionsDescriptor restrictionOptions = null; 249 if (lifeCycleState != null) { 250 restrictionOptions = restrictions.getRestrictionOption(lifeCycleState); 251 } 252 if (restrictionOptions == null) { 253 // try to get restriction for any life cycle states 254 restrictionOptions = restrictions.getRestrictionOption("*"); 255 } 256 if (restrictionOptions != null) { 257 return restrictionOptions.getOptions(); 258 } 259 } 260 return null; 261 } 262 263 protected VersioningOption validateOption(Document doc, VersioningOption option) { 264 List<VersioningOption> options = getSaveOptions(doc); 265 // some variables for exceptions 266 String type = doc.getType().getName(); 267 String lifeCycleState; 268 try { 269 lifeCycleState = doc.getLifeCycleState(); 270 } catch (LifeCycleException e) { 271 lifeCycleState = null; 272 } 273 if (option == null) { 274 if (options.isEmpty() || options.contains(NONE)) { 275 // Valid cases: 276 // - we don't ask for a version and versioning is blocked by configuration 277 // - we don't ask for a version and NONE is available as restriction 278 return NONE; 279 } else { 280 // No version is asked but configuration requires that document must be versioned ie: NONE doesn't 281 // appear in restriction contribution 282 throw new NuxeoException("Versioning configuration restricts documents with type=" + type 283 + "/lifeCycleState=" + lifeCycleState + " must be versioned for each updates."); 284 } 285 } else if (!options.contains(option)) { 286 throw new NuxeoException("Versioning option=" + option + " is not allowed by the configuration for type=" 287 + type + "/lifeCycleState=" + lifeCycleState); 288 } 289 return option; 290 } 291 292 @Override 293 public boolean isPreSaveDoingCheckOut(Document doc, boolean isDirty, VersioningOption option, 294 Map<String, Serializable> options) { 295 boolean disableAutoCheckOut = Boolean.TRUE.equals(options.get(VersioningService.DISABLE_AUTO_CHECKOUT)); 296 return !doc.isCheckedOut() && isDirty && !disableAutoCheckOut; 297 } 298 299 @Override 300 public VersioningOption doPreSave(Document doc, boolean isDirty, VersioningOption option, String checkinComment, 301 Map<String, Serializable> options) { 302 option = validateOption(doc, option); 303 if (isPreSaveDoingCheckOut(doc, isDirty, option, options)) { 304 doCheckOut(doc); 305 followTransitionByOption(doc, option); 306 } 307 // transition follow shouldn't change what postSave options will be 308 return option; 309 } 310 311 protected void followTransitionByOption(Document doc, VersioningOption option) { 312 String lifecycleState = doc.getLifeCycleState(); 313 if (APPROVED_STATE.equals(lifecycleState) || OBSOLETE_STATE.equals(lifecycleState)) { 314 doc.followTransition(BACK_TO_PROJECT_TRANSITION); 315 } 316 } 317 318 @Override 319 public boolean isPostSaveDoingCheckIn(Document doc, VersioningOption option, Map<String, Serializable> options) { 320 // option = validateOption(doc, option); // validated before 321 return doc.isCheckedOut() && option != NONE; 322 } 323 324 @Override 325 public Document doPostSave(Document doc, VersioningOption option, String checkinComment, 326 Map<String, Serializable> options) { 327 if (isPostSaveDoingCheckIn(doc, option, options)) { 328 incrementByOption(doc, option); 329 return doc.checkIn(null, checkinComment); // auto-label 330 } 331 return null; 332 } 333 334 @Override 335 public Document doCheckIn(Document doc, VersioningOption option, String checkinComment) { 336 if (option != NONE) { 337 incrementByOption(doc, option == MAJOR ? MAJOR : MINOR); 338 } 339 return doc.checkIn(null, checkinComment); // auto-label 340 } 341 342 @Override 343 public void doCheckOut(Document doc) { 344 Document base = doc.getBaseVersion(); 345 doc.checkOut(); 346 // set version number to that of the latest version 347 if (base.isLatestVersion()) { 348 // nothing to do, already at proper version 349 } else { 350 // this doc was restored from a non-latest version, find the latest one 351 Document last = doc.getLastVersion(); 352 if (last != null) { 353 try { 354 setVersion(doc, getMajor(last), getMinor(last)); 355 } catch (PropertyNotFoundException e) { 356 // ignore 357 } 358 } 359 } 360 } 361 362 @Override 363 @Deprecated 364 public Map<String, VersioningRuleDescriptor> getVersioningRules() { 365 return Collections.emptyMap(); 366 } 367 368 @Override 369 @Deprecated 370 public void setVersioningRules(Map<String, VersioningRuleDescriptor> versioningRules) { 371 // Convert former rules to new one - keep initial state and restriction 372 int order = DEFAULT_FORMER_RULE_ORDER - 1; 373 for (Entry<String, VersioningRuleDescriptor> rules : versioningRules.entrySet()) { 374 String documentType = rules.getKey(); 375 VersioningRuleDescriptor versioningRule = rules.getValue(); 376 // Compute policy and filter id 377 String compatId = COMPAT_ID_PREFIX + documentType; 378 379 // Convert the rule 380 if (versioningRule.isEnabled()) { 381 VersioningPolicyDescriptor policy = new VersioningPolicyDescriptor(); 382 policy.id = compatId; 383 policy.order = order; 384 policy.initialState = versioningRule.initialState; 385 policy.filterIds = new ArrayList<>(Collections.singleton(compatId)); 386 387 VersioningFilterDescriptor filter = new VersioningFilterDescriptor(); 388 filter.id = compatId; 389 filter.types = Collections.singleton(documentType); 390 391 // Register rules 392 versioningPolicies.put(compatId, policy); 393 versioningFilters.put(compatId, filter); 394 395 // Convert save options 396 VersioningRestrictionDescriptor restriction = new VersioningRestrictionDescriptor(); 397 restriction.type = documentType; 398 restriction.options = versioningRule.getOptions() 399 .values() 400 .stream() 401 .map(SaveOptionsDescriptor::toRestrictionOptions) 402 .collect(Collectors.toMap( 403 VersioningRestrictionOptionsDescriptor::getLifeCycleState, 404 Function.identity())); 405 versioningRestrictions.put(restriction.type, restriction); 406 407 order--; 408 } else { 409 versioningPolicies.remove(compatId); 410 versioningFilters.remove(compatId); 411 } 412 } 413 } 414 415 @Override 416 @Deprecated 417 public void setDefaultVersioningRule(DefaultVersioningRuleDescriptor defaultVersioningRule) { 418 if (defaultVersioningRule == null) { 419 return; 420 } 421 // Convert former rules to new one - keep initial state and restriction 422 VersioningPolicyDescriptor policy = new VersioningPolicyDescriptor(); 423 policy.id = COMPAT_DEFAULT_ID; 424 policy.order = DEFAULT_FORMER_RULE_ORDER; 425 policy.initialState = defaultVersioningRule.initialState; 426 427 // Register rule 428 if (versioningPolicies == null) { 429 versioningPolicies = new HashMap<>(); 430 } 431 versioningPolicies.put(policy.id, policy); 432 433 // Convert save options 434 VersioningRestrictionDescriptor restriction = new VersioningRestrictionDescriptor(); 435 restriction.type = "*"; 436 restriction.options = defaultVersioningRule.getOptions() 437 .values() 438 .stream() 439 .map(SaveOptionsDescriptor::toRestrictionOptions) 440 .collect(Collectors.toMap( 441 VersioningRestrictionOptionsDescriptor::getLifeCycleState, 442 Function.identity())); 443 versioningRestrictions.put(restriction.type, restriction); 444 } 445 446 @Override 447 public void setVersioningPolicies(Map<String, VersioningPolicyDescriptor> versioningPolicies) { 448 this.versioningPolicies.clear(); 449 if (versioningPolicies != null) { 450 this.versioningPolicies.putAll(versioningPolicies); 451 } 452 } 453 454 @Override 455 public void setVersioningFilters(Map<String, VersioningFilterDescriptor> versioningFilters) { 456 this.versioningFilters.clear(); 457 if (versioningFilters != null) { 458 this.versioningFilters.putAll(versioningFilters); 459 } 460 } 461 462 @Override 463 public void setVersioningRestrictions(Map<String, VersioningRestrictionDescriptor> versioningRestrictions) { 464 this.versioningRestrictions.clear(); 465 if (versioningRestrictions != null) { 466 this.versioningRestrictions.putAll(versioningRestrictions); 467 } 468 } 469 470 @Override 471 public void doAutomaticVersioning(DocumentModel previousDocument, DocumentModel currentDocument, boolean before) { 472 VersioningPolicyDescriptor policy = retrieveMatchingVersioningPolicy(previousDocument, currentDocument, before); 473 if (policy != null && policy.getIncrement() != NONE) { 474 if (before) { 475 if (previousDocument.isCheckedOut()) { 476 previousDocument.checkIn(policy.getIncrement(), null); // auto label 477 // put back document in checked out state 478 previousDocument.checkOut(); 479 } 480 } else { 481 if (currentDocument.isCheckedOut()) { 482 currentDocument.checkIn(policy.getIncrement(), null); // auto label 483 } 484 } 485 } 486 } 487 488 protected VersioningPolicyDescriptor retrieveMatchingVersioningPolicy(DocumentModel previousDocument, 489 DocumentModel currentDocument, boolean before) { 490 return versioningPolicies.values() 491 .stream() 492 .sorted() 493 .filter(policy -> policy.isBeforeUpdate() == before) 494 .filter(policy -> isPolicyMatch(policy, previousDocument, currentDocument)) 495 // Filter out policy with null increment - possible if we declare a policy for the 496 // initial state for all documents 497 .filter(policy -> policy.getIncrement() != null) 498 .findFirst() 499 .orElse(null); 500 } 501 502 protected boolean isPolicyMatch(VersioningPolicyDescriptor policyDescriptor, DocumentModel previousDocument, 503 DocumentModel currentDocument) { 504 // Relation between filters in a policy is a AND 505 for (String filterId : policyDescriptor.getFilterIds()) { 506 VersioningFilterDescriptor filterDescriptor = versioningFilters.get(filterId); 507 if (filterDescriptor == null) { 508 // TODO maybe throw something ? 509 log.warn("Versioning filter with id=" + filterId + " is referenced in the policy with id= " 510 + policyDescriptor.getId() + ", but doesn't exist."); 511 } else if (!filterDescriptor.newInstance().test(previousDocument, currentDocument)) { 512 // As it's a AND, if one fails then policy doesn't match 513 return false; 514 } 515 } 516 // All filters match the context (previousDocument + currentDocument) 517 return true; 518 } 519 520}