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 java.io.Serializable;
023import java.util.ArrayDeque;
024import java.util.Deque;
025import java.util.LinkedHashMap;
026import java.util.List;
027import java.util.Map;
028import java.util.stream.Collectors;
029
030import org.apache.commons.logging.Log;
031import org.apache.commons.logging.LogFactory;
032import org.nuxeo.ecm.core.api.CoreSession;
033import org.nuxeo.ecm.core.api.DocumentModel;
034import org.nuxeo.ecm.core.api.VersioningOption;
035import org.nuxeo.ecm.core.model.Document;
036import org.nuxeo.runtime.RuntimeMessage.Level;
037import org.nuxeo.runtime.RuntimeMessage.Source;
038import org.nuxeo.runtime.logging.DeprecationLogger;
039import org.nuxeo.runtime.model.ComponentContext;
040import org.nuxeo.runtime.model.ComponentInstance;
041import org.nuxeo.runtime.model.ComponentName;
042import org.nuxeo.runtime.model.DefaultComponent;
043import org.nuxeo.runtime.model.SimpleContributionRegistry;
044
045/**
046 * Versioning service component and implementation.
047 */
048public class VersioningComponent extends DefaultComponent implements VersioningService {
049
050    private static final Log log = LogFactory.getLog(VersioningComponent.class);
051
052    public static final String VERSIONING_SERVICE_XP = "versioningService";
053
054    public static final String VERSIONING_RULE_XP = "versioningRules";
055
056    public static final String VERSIONING_POLICY_XP = "policies";
057
058    public static final String VERSIONING_FILTER_XP = "filters";
059
060    public static final String VERSIONING_RESTRICTION_XP = "restrictions";
061
062    protected static final StandardVersioningService STANDARD_VERSIONING_SERVICE = new StandardVersioningService();
063
064    protected Map<VersioningServiceDescriptor, VersioningService> versioningServices = new LinkedHashMap<>();
065
066    /**
067     * @deprecated since 9.1 use 'policy', 'filter' and 'restriction' contributions instead
068     */
069    @Deprecated
070    protected VersioningRuleRegistry versioningRulesRegistry = new VersioningRuleRegistry();
071
072    protected VersioningPolicyRegistry versioningPoliciesRegistry = new VersioningPolicyRegistry();
073
074    protected VersioningFilterRegistry versioningFiltersRegistry = new VersioningFilterRegistry();
075
076    protected VersioningRestrictionRegistry versioningRestrictionsRegistry = new VersioningRestrictionRegistry();
077
078    protected static class VersioningPolicyRegistry extends SimpleContributionRegistry<VersioningPolicyDescriptor> {
079
080        @Override
081        public String getContributionId(VersioningPolicyDescriptor contrib) {
082            return contrib.getId();
083        }
084
085        public Map<String, VersioningPolicyDescriptor> getVersioningPolicyDescriptors() {
086            return currentContribs.entrySet()
087                                  .stream()
088                                  .sorted(Map.Entry.comparingByValue())
089                                  .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1,
090                                          LinkedHashMap::new));
091        }
092
093    }
094
095    protected static class VersioningFilterRegistry extends SimpleContributionRegistry<VersioningFilterDescriptor> {
096
097        @Override
098        public String getContributionId(VersioningFilterDescriptor contrib) {
099            return contrib.getId();
100        }
101
102        public Map<String, VersioningFilterDescriptor> getVersioningFilterDescriptors() {
103            return currentContribs;
104        }
105
106    }
107
108    protected static class VersioningRestrictionRegistry
109            extends SimpleContributionRegistry<VersioningRestrictionDescriptor> {
110
111        @Override
112        public String getContributionId(VersioningRestrictionDescriptor contrib) {
113            return contrib.getType();
114        }
115
116        public Map<String, VersioningRestrictionDescriptor> getVersioningRestrictionDescriptors() {
117            return currentContribs;
118        }
119
120    }
121
122    /**
123     * @deprecated since 9.1 use 'policy', 'filter' and 'restriction' contributions instead
124     */
125    @Deprecated
126    protected static class VersioningRuleRegistry extends SimpleContributionRegistry<VersioningRuleDescriptor> {
127
128        @Override
129        public String getContributionId(VersioningRuleDescriptor contrib) {
130            return contrib.getTypeName();
131        }
132
133        @Override
134        public VersioningRuleDescriptor clone(VersioningRuleDescriptor orig) {
135            return new VersioningRuleDescriptor(orig);
136        }
137
138        @Override
139        public void merge(VersioningRuleDescriptor src, VersioningRuleDescriptor dst) {
140            dst.merge(src);
141        }
142
143        @Override
144        public boolean isSupportingMerge() {
145            return true;
146        }
147
148        @Override
149        public void contributionUpdated(String id, VersioningRuleDescriptor contrib,
150                VersioningRuleDescriptor newOrigContrib) {
151            if (contrib.isEnabled()) {
152                currentContribs.put(id, contrib);
153            } else {
154                currentContribs.remove(id);
155            }
156        }
157
158        public void clear() {
159            currentContribs.clear();
160        }
161
162        public Map<String, VersioningRuleDescriptor> getVersioningRuleDescriptors() {
163            return currentContribs;
164        }
165    }
166
167    /**
168     * @deprecated since 9.1 use 'policy', 'filter' and 'restriction' contributions instead
169     */
170    @Deprecated
171    protected Deque<DefaultVersioningRuleDescriptor> defaultVersioningRuleList = new ArrayDeque<>();
172
173    // public for tests
174    public VersioningService service = null;
175
176    protected ComponentContext context;
177
178    @Override
179    public void activate(ComponentContext context) {
180        this.context = context;
181        this.service = STANDARD_VERSIONING_SERVICE;
182    }
183
184    @Override
185    public void deactivate(ComponentContext context) {
186        this.context = null;
187        this.service = null;
188    }
189
190    @Override
191    public void registerContribution(Object contrib, String point, ComponentInstance contributor) {
192        switch (point) {
193        case VERSIONING_SERVICE_XP:
194            registerVersioningService((VersioningServiceDescriptor) contrib);
195            break;
196        case VERSIONING_RULE_XP:
197            if (contrib instanceof VersioningRuleDescriptor) {
198                VersioningRuleDescriptor rule = (VersioningRuleDescriptor) contrib;
199                registerVersioningRule(rule);
200                ComponentName compName = contributor.getName();
201                String message = String.format(
202                        "Versioning rule for '%s' on component %s should now be contributed to extension points '%s', "
203                                + "'%s' and '%s': a compatibility registration was performed but it may not be accurate.",
204                        (rule).getTypeName(), compName, VERSIONING_POLICY_XP, VERSIONING_FILTER_XP,
205                        VERSIONING_RESTRICTION_XP);
206                DeprecationLogger.log(message, "9.1");
207                addRuntimeMessage(Level.WARNING, message, Source.EXTENSION, compName.getName());
208            } else if (contrib instanceof DefaultVersioningRuleDescriptor) {
209                registerDefaultVersioningRule((DefaultVersioningRuleDescriptor) contrib);
210                ComponentName compName = contributor.getName();
211                String message = String.format("Default versioning rule on component %s should now be contributed to "
212                        + "extension points '%s' and '%s': a compatibility registration was performed but it may not be "
213                        + "accurate.", compName, VERSIONING_POLICY_XP, VERSIONING_RESTRICTION_XP);
214                DeprecationLogger.log(message, "9.1");
215                addRuntimeMessage(Level.WARNING, message, Source.EXTENSION, compName.getName());
216            } else {
217                throw new RuntimeException("Unknown contribution to " + point + ": " + contrib.getClass());
218            }
219            break;
220        case VERSIONING_POLICY_XP:
221            registerVersioningPolicy((VersioningPolicyDescriptor) contrib);
222            break;
223        case VERSIONING_FILTER_XP:
224            registerVersioningFilter((VersioningFilterDescriptor) contrib);
225            break;
226        case VERSIONING_RESTRICTION_XP:
227            registerVersioningRestriction((VersioningRestrictionDescriptor) contrib);
228            break;
229        default:
230            throw new RuntimeException("Unknown extension point: " + point);
231        }
232    }
233
234    @Override
235    public void unregisterContribution(Object contrib, String point, ComponentInstance contributor) {
236        switch (point) {
237        case VERSIONING_SERVICE_XP:
238            unregisterVersioningService((VersioningServiceDescriptor) contrib);
239            break;
240        case VERSIONING_RULE_XP:
241            if (contrib instanceof VersioningRuleDescriptor) {
242                unregisterVersioningRule((VersioningRuleDescriptor) contrib);
243            } else if (contrib instanceof DefaultVersioningRuleDescriptor) {
244                unregisterDefaultVersioningRule((DefaultVersioningRuleDescriptor) contrib);
245            }
246            break;
247        case VERSIONING_POLICY_XP:
248            unregisterVersioningPolicy((VersioningPolicyDescriptor) contrib);
249            break;
250        case VERSIONING_FILTER_XP:
251            unregisterVersioningFilter((VersioningFilterDescriptor) contrib);
252            break;
253        case VERSIONING_RESTRICTION_XP:
254            unregisterVersioningRestriction((VersioningRestrictionDescriptor) contrib);
255            break;
256        default:
257            break;
258        }
259    }
260
261    protected void registerVersioningService(VersioningServiceDescriptor contrib) {
262        String klass = contrib.className;
263        try {
264            VersioningService vs = (VersioningService) context.getRuntimeContext()
265                                                              .loadClass(klass)
266                                                              .getDeclaredConstructor()
267                                                              .newInstance();
268            versioningServices.put(contrib, vs);
269        } catch (ReflectiveOperationException e) {
270            throw new RuntimeException("Failed to instantiate: " + klass, e);
271        }
272        log.info("Registered versioning service: " + klass);
273        recompute();
274    }
275
276    protected void unregisterVersioningService(VersioningServiceDescriptor contrib) {
277        versioningServices.remove(contrib);
278        log.info("Unregistered versioning service: " + contrib.className);
279        recompute();
280    }
281
282    /**
283     * @deprecated since 9.1, use policy and filter contributions instead
284     */
285    @Deprecated
286    protected void registerVersioningRule(VersioningRuleDescriptor contrib) {
287        versioningRulesRegistry.addContribution(contrib);
288        log.info("Registered versioning rule: " + contrib.getTypeName());
289        recompute();
290    }
291
292    /**
293     * @deprecated since 9.1, use policy and filter contributions instead
294     */
295    @Deprecated
296    protected void unregisterVersioningRule(VersioningRuleDescriptor contrib) {
297        versioningRulesRegistry.removeContribution(contrib);
298        log.info("Unregistered versioning rule: " + contrib.getTypeName());
299        recompute();
300    }
301
302    /**
303     * @deprecated since 9.1, use policy and filter contributions instead
304     */
305    @Deprecated
306    protected void registerDefaultVersioningRule(DefaultVersioningRuleDescriptor contrib) {
307        // could use a linked set instead, but given the size a linked list is enough
308        defaultVersioningRuleList.add(contrib);
309        recompute();
310    }
311
312    /**
313     * @deprecated since 9.1, use policy and filter contributions instead
314     */
315    @Deprecated
316    protected void unregisterDefaultVersioningRule(DefaultVersioningRuleDescriptor contrib) {
317        defaultVersioningRuleList.remove(contrib);
318        recompute();
319    }
320
321    protected void registerVersioningPolicy(VersioningPolicyDescriptor contrib) {
322        versioningPoliciesRegistry.addContribution(contrib);
323        log.info("Registered versioning policy: " + contrib.getId());
324        recompute();
325    }
326
327    protected void unregisterVersioningPolicy(VersioningPolicyDescriptor contrib) {
328        versioningPoliciesRegistry.removeContribution(contrib);
329        log.info("Unregistered versioning policy: " + contrib.getId());
330        recompute();
331    }
332
333    protected void registerVersioningFilter(VersioningFilterDescriptor contrib) {
334        versioningFiltersRegistry.addContribution(contrib);
335        log.info("Registered versioning filter: " + contrib.getId());
336        recompute();
337    }
338
339    protected void unregisterVersioningFilter(VersioningFilterDescriptor contrib) {
340        versioningFiltersRegistry.removeContribution(contrib);
341        log.info("Unregistered versioning filter: " + contrib.getId());
342        recompute();
343    }
344
345    protected void registerVersioningRestriction(VersioningRestrictionDescriptor contrib) {
346        versioningRestrictionsRegistry.addContribution(contrib);
347        log.info("Registered versioning restriction: " + contrib.getType());
348        recompute();
349    }
350
351    protected void unregisterVersioningRestriction(VersioningRestrictionDescriptor contrib) {
352        versioningRestrictionsRegistry.removeContribution(contrib);
353        log.info("Unregistered versioning restriction: " + contrib.getType());
354        recompute();
355    }
356
357    protected void recompute() {
358        VersioningService versioningService = STANDARD_VERSIONING_SERVICE;
359        for (VersioningService vs : versioningServices.values()) {
360            versioningService = vs;
361        }
362        if (versioningService instanceof ExtendableVersioningService) {
363            ExtendableVersioningService evs = (ExtendableVersioningService) versioningService;
364            evs.setVersioningPolicies(getVersioningPolicies());
365            evs.setVersioningFilters(getVersioningFilters());
366            evs.setVersioningRestrictions(getVersioningRestrictions());
367            evs.setVersioningRules(getVersioningRules());
368            evs.setDefaultVersioningRule(getDefaultVersioningRule());
369        }
370        this.service = versioningService;
371    }
372
373    /**
374     * @deprecated since 9.1, use policy and filter contributions instead
375     */
376    @Deprecated
377    protected Map<String, VersioningRuleDescriptor> getVersioningRules() {
378        return versioningRulesRegistry.getVersioningRuleDescriptors();
379    }
380
381    /**
382     * @deprecated since 9.1, use policy and filter contributions instead
383     */
384    @Deprecated
385    protected DefaultVersioningRuleDescriptor getDefaultVersioningRule() {
386        return defaultVersioningRuleList.peekLast();
387    }
388
389    protected Map<String, VersioningPolicyDescriptor> getVersioningPolicies() {
390        return versioningPoliciesRegistry.getVersioningPolicyDescriptors();
391    }
392
393    protected Map<String, VersioningFilterDescriptor> getVersioningFilters() {
394        return versioningFiltersRegistry.getVersioningFilterDescriptors();
395    }
396
397    protected Map<String, VersioningRestrictionDescriptor> getVersioningRestrictions() {
398        return versioningRestrictionsRegistry.getVersioningRestrictionDescriptors();
399    }
400
401    @Override
402    public String getVersionLabel(DocumentModel doc) {
403        return service.getVersionLabel(doc);
404    }
405
406    @Override
407    public void doPostCreate(Document doc, Map<String, Serializable> options) {
408        service.doPostCreate(doc, options);
409    }
410
411    @Override
412    public List<VersioningOption> getSaveOptions(DocumentModel docModel) {
413        return service.getSaveOptions(docModel);
414    }
415
416    @Override
417    public boolean isPreSaveDoingCheckOut(Document doc, boolean isDirty, VersioningOption option,
418            Map<String, Serializable> options) {
419        return service.isPreSaveDoingCheckOut(doc, isDirty, option, options);
420    }
421
422    @Override
423    public VersioningOption doPreSave(CoreSession session, Document doc, boolean isDirty, VersioningOption option,
424            String checkinComment, Map<String, Serializable> options) {
425        return service.doPreSave(session, doc, isDirty, option, checkinComment, options);
426    }
427
428    @Override
429    public boolean isPostSaveDoingCheckIn(Document doc, VersioningOption option, Map<String, Serializable> options) {
430        return service.isPostSaveDoingCheckIn(doc, option, options);
431    }
432
433    @Override
434    public Document doPostSave(CoreSession session, Document doc, VersioningOption option, String checkinComment,
435            Map<String, Serializable> options) {
436        return service.doPostSave(session, doc, option, checkinComment, options);
437    }
438
439    @Override
440    public Document doCheckIn(Document doc, VersioningOption option, String checkinComment) {
441        return service.doCheckIn(doc, option, checkinComment);
442    }
443
444    @Override
445    public void doCheckOut(Document doc) {
446        service.doCheckOut(doc);
447    }
448
449    @Override
450    public void doAutomaticVersioning(DocumentModel previousDocument, DocumentModel currentDocument, boolean before) {
451        service.doAutomaticVersioning(previousDocument, currentDocument, before);
452    }
453
454}