001/*
002 * Copyright (c) 2006-2011 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the Eclipse Public License v1.0
006 * which accompanies this distribution, and is available at
007 * http://www.eclipse.org/legal/epl-v10.html
008 *
009 * Contributors:
010 *     Florent Guillaume
011 *     Laurent Doguin
012 */
013package org.nuxeo.ecm.core.versioning;
014
015import static org.nuxeo.ecm.core.api.VersioningOption.MAJOR;
016import static org.nuxeo.ecm.core.api.VersioningOption.MINOR;
017import static org.nuxeo.ecm.core.api.VersioningOption.NONE;
018
019import java.io.Serializable;
020import java.util.Arrays;
021import java.util.List;
022import java.util.Map;
023
024import org.apache.commons.logging.Log;
025import org.apache.commons.logging.LogFactory;
026import org.nuxeo.ecm.core.api.DocumentModel;
027import org.nuxeo.ecm.core.api.LifeCycleException;
028import org.nuxeo.ecm.core.api.VersioningOption;
029import org.nuxeo.ecm.core.api.model.PropertyNotFoundException;
030import org.nuxeo.ecm.core.model.Document;
031import org.nuxeo.ecm.core.schema.FacetNames;
032
033/**
034 * Implementation of the versioning service that follows standard checkout / checkin semantics.
035 */
036public class StandardVersioningService implements ExtendableVersioningService {
037
038    private static final Log log = LogFactory.getLog(StandardVersioningService.class);
039
040    public static final String FILE_TYPE = "File";
041
042    public static final String NOTE_TYPE = "Note";
043
044    public static final String PROJECT_STATE = "project";
045
046    public static final String APPROVED_STATE = "approved";
047
048    public static final String OBSOLETE_STATE = "obsolete";
049
050    public static final String BACK_TO_PROJECT_TRANSITION = "backToProject";
051
052    protected static final String AUTO_CHECKED_OUT = "AUTO_CHECKED_OUT";
053
054    /** Key for major version in Document API. */
055    protected static final String MAJOR_VERSION = "ecm:majorVersion";
056
057    /** Key for minor version in Document API. */
058    protected static final String MINOR_VERSION = "ecm:minorVersion";
059
060    private Map<String, VersioningRuleDescriptor> versioningRules;
061
062    private DefaultVersioningRuleDescriptor defaultVersioningRule;
063
064    @Override
065    public String getVersionLabel(DocumentModel docModel) {
066        String label;
067        try {
068            label = getMajor(docModel) + "." + getMinor(docModel);
069            if (docModel.isCheckedOut() && !"0.0".equals(label)) {
070                label += "+";
071            }
072        } catch (PropertyNotFoundException e) {
073            label = "";
074        }
075        return label;
076    }
077
078    protected long getMajor(DocumentModel docModel) {
079        return getVersion(docModel, VersioningService.MAJOR_VERSION_PROP);
080    }
081
082    protected long getMinor(DocumentModel docModel) {
083        return getVersion(docModel, VersioningService.MINOR_VERSION_PROP);
084    }
085
086    protected long getVersion(DocumentModel docModel, String prop) {
087        Object propVal = docModel.getPropertyValue(prop);
088        if (propVal == null || !(propVal instanceof Long)) {
089            return 0;
090        } else {
091            return ((Long) propVal).longValue();
092        }
093    }
094
095    protected long getMajor(Document doc) {
096        return getVersion(doc, MAJOR_VERSION);
097    }
098
099    protected long getMinor(Document doc) {
100        return getVersion(doc, MINOR_VERSION);
101    }
102
103    protected long getVersion(Document doc, String prop) {
104        Object propVal = doc.getPropertyValue(prop);
105        if (propVal == null || !(propVal instanceof Long)) {
106            return 0;
107        } else {
108            return ((Long) propVal).longValue();
109        }
110    }
111
112    protected void setVersion(Document doc, long major, long minor) {
113        doc.setPropertyValue(MAJOR_VERSION, Long.valueOf(major));
114        doc.setPropertyValue(MINOR_VERSION, Long.valueOf(minor));
115    }
116
117    protected void incrementMajor(Document doc) {
118        setVersion(doc, getMajor(doc) + 1, 0);
119    }
120
121    protected void incrementMinor(Document doc) {
122        doc.setPropertyValue(MINOR_VERSION, Long.valueOf(getMinor(doc) + 1));
123    }
124
125    protected void incrementByOption(Document doc, VersioningOption option) {
126        try {
127            if (option == MAJOR) {
128                incrementMajor(doc);
129            } else if (option == MINOR) {
130                incrementMinor(doc);
131            }
132            // else nothing
133        } catch (PropertyNotFoundException e) {
134            // ignore
135        }
136    }
137
138    @Override
139    public void doPostCreate(Document doc, Map<String, Serializable> options) {
140        if (doc.isVersion() || doc.isProxy()) {
141            return;
142        }
143        setInitialVersion(doc);
144    }
145
146    /**
147     * Sets the initial version on a document. Can be overridden.
148     */
149    protected void setInitialVersion(Document doc) {
150        InitialStateDescriptor initialState = null;
151        if (versioningRules != null) {
152            VersioningRuleDescriptor versionRule = versioningRules.get(doc.getType().getName());
153            if (versionRule != null) {
154                initialState = versionRule.getInitialState();
155            }
156        }
157        if (initialState == null && defaultVersioningRule != null) {
158            initialState = defaultVersioningRule.getInitialState();
159        }
160        if (initialState != null) {
161            int initialMajor = initialState.getMajor();
162            int initialMinor = initialState.getMinor();
163            setVersion(doc, initialMajor, initialMinor);
164            return;
165        }
166        setVersion(doc, 0, 0);
167    }
168
169    @Override
170    public List<VersioningOption> getSaveOptions(DocumentModel docModel) {
171        boolean versionable = docModel.isVersionable();
172        String lifecycleState = docModel.getCoreSession().getCurrentLifeCycleState(docModel.getRef());
173        String type = docModel.getType();
174        return getSaveOptions(versionable, lifecycleState, type);
175    }
176
177    protected List<VersioningOption> getSaveOptions(Document doc) {
178        boolean versionable = doc.getType().getFacets().contains(FacetNames.VERSIONABLE);
179        String lifecycleState;
180        try {
181            lifecycleState = doc.getLifeCycleState();
182        } catch (LifeCycleException e) {
183            lifecycleState = null;
184        }
185        String type = doc.getType().getName();
186        return getSaveOptions(versionable, lifecycleState, type);
187    }
188
189    protected List<VersioningOption> getSaveOptions(boolean versionable, String lifecycleState, String type) {
190        if (!versionable) {
191            return Arrays.asList(NONE);
192        }
193        if (lifecycleState == null) {
194            return Arrays.asList(NONE);
195        }
196        SaveOptionsDescriptor option = null;
197        if (versioningRules != null) {
198            VersioningRuleDescriptor saveOption = versioningRules.get(type);
199            if (saveOption != null) {
200                option = saveOption.getOptions().get(lifecycleState);
201                if (option == null) {
202                    // try on any life cycle state
203                    option = saveOption.getOptions().get("*");
204                }
205            }
206        }
207        if (option == null && defaultVersioningRule != null) {
208            option = defaultVersioningRule.getOptions().get(lifecycleState);
209            if (option == null) {
210                // try on any life cycle state
211                option = defaultVersioningRule.getOptions().get("*");
212            }
213        }
214        if (option != null) {
215            return option.getVersioningOptionList();
216        }
217        if (PROJECT_STATE.equals(lifecycleState) || APPROVED_STATE.equals(lifecycleState)
218                || OBSOLETE_STATE.equals(lifecycleState)) {
219            return Arrays.asList(NONE, MINOR, MAJOR);
220        }
221        if (FILE_TYPE.equals(type) || NOTE_TYPE.equals(type)) {
222            return Arrays.asList(NONE, MINOR, MAJOR);
223        }
224        return Arrays.asList(NONE);
225    }
226
227    protected VersioningOption validateOption(Document doc, VersioningOption option) {
228        List<VersioningOption> options = getSaveOptions(doc);
229        if (!options.contains(option)) {
230            option = options.isEmpty() ? NONE : options.get(0);
231        }
232        return option;
233    }
234
235    @Override
236    public boolean isPreSaveDoingCheckOut(Document doc, boolean isDirty, VersioningOption option,
237            Map<String, Serializable> options) {
238        boolean disableAutoCheckOut = Boolean.TRUE.equals(options.get(VersioningService.DISABLE_AUTO_CHECKOUT));
239        return !doc.isCheckedOut() && isDirty && !disableAutoCheckOut;
240    }
241
242    @Override
243    public VersioningOption doPreSave(Document doc, boolean isDirty, VersioningOption option, String checkinComment,
244            Map<String, Serializable> options) {
245        option = validateOption(doc, option);
246        if (isPreSaveDoingCheckOut(doc, isDirty, option, options)) {
247            doCheckOut(doc);
248            followTransitionByOption(doc, option);
249        }
250        // transition follow shouldn't change what postSave options will be
251        return option;
252    }
253
254    protected void followTransitionByOption(Document doc, VersioningOption option) {
255        String lifecycleState = doc.getLifeCycleState();
256        if (APPROVED_STATE.equals(lifecycleState) || OBSOLETE_STATE.equals(lifecycleState)) {
257            doc.followTransition(BACK_TO_PROJECT_TRANSITION);
258        }
259    }
260
261    @Override
262    public boolean isPostSaveDoingCheckIn(Document doc, VersioningOption option, Map<String, Serializable> options) {
263        // option = validateOption(doc, option); // validated before
264        return doc.isCheckedOut() && option != NONE;
265    }
266
267    @Override
268    public Document doPostSave(Document doc, VersioningOption option, String checkinComment,
269            Map<String, Serializable> options) {
270        if (isPostSaveDoingCheckIn(doc, option, options)) {
271            incrementByOption(doc, option);
272            return doc.checkIn(null, checkinComment); // auto-label
273        }
274        return null;
275    }
276
277    @Override
278    public Document doCheckIn(Document doc, VersioningOption option, String checkinComment) {
279        if (option != VersioningOption.NONE) {
280            incrementByOption(doc, option == MAJOR ? MAJOR : MINOR);
281        }
282        return doc.checkIn(null, checkinComment); // auto-label
283    }
284
285    @Override
286    public void doCheckOut(Document doc) {
287        doc.checkOut();
288        // set version number to that of the last version
289        try {
290            Document last = doc.getLastVersion();
291            if (last != null) {
292                setVersion(doc, getMajor(last), getMinor(last));
293            }
294        } catch (PropertyNotFoundException e) {
295            // ignore
296        }
297    }
298
299    @Override
300    public Map<String, VersioningRuleDescriptor> getVersioningRules() {
301        return versioningRules;
302    }
303
304    @Override
305    public void setVersioningRules(Map<String, VersioningRuleDescriptor> versioningRules) {
306        this.versioningRules = versioningRules;
307    }
308
309    @Override
310    public void setDefaultVersioningRule(DefaultVersioningRuleDescriptor defaultVersioningRule) {
311        this.defaultVersioningRule = defaultVersioningRule;
312    }
313
314}