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