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        // make sure major is not null by re-setting it
130        setVersion(doc, getMajor(doc), getMinor(doc) + 1);
131    }
132
133    protected void incrementByOption(Document doc, VersioningOption option) {
134        try {
135            if (option == MAJOR) {
136                incrementMajor(doc);
137            } else if (option == MINOR) {
138                incrementMinor(doc);
139            }
140            // else nothing
141        } catch (PropertyNotFoundException e) {
142            // ignore
143        }
144    }
145
146    @Override
147    public void doPostCreate(Document doc, Map<String, Serializable> options) {
148        if (doc.isVersion() || doc.isProxy()) {
149            return;
150        }
151        setInitialVersion(doc);
152    }
153
154    /**
155     * Sets the initial version on a document. Can be overridden.
156     */
157    protected void setInitialVersion(Document doc) {
158        InitialStateDescriptor initialState = null;
159        if (versioningRules != null) {
160            VersioningRuleDescriptor versionRule = versioningRules.get(doc.getType().getName());
161            if (versionRule != null) {
162                initialState = versionRule.getInitialState();
163            }
164        }
165        if (initialState == null && defaultVersioningRule != null) {
166            initialState = defaultVersioningRule.getInitialState();
167        }
168        if (initialState != null) {
169            int initialMajor = initialState.getMajor();
170            int initialMinor = initialState.getMinor();
171            setVersion(doc, initialMajor, initialMinor);
172            return;
173        }
174        setVersion(doc, 0, 0);
175    }
176
177    @Override
178    public List<VersioningOption> getSaveOptions(DocumentModel docModel) {
179        boolean versionable = docModel.isVersionable();
180        String lifecycleState = docModel.getCoreSession().getCurrentLifeCycleState(docModel.getRef());
181        String type = docModel.getType();
182        return getSaveOptions(versionable, lifecycleState, type);
183    }
184
185    protected List<VersioningOption> getSaveOptions(Document doc) {
186        boolean versionable = doc.getType().getFacets().contains(FacetNames.VERSIONABLE);
187        String lifecycleState;
188        try {
189            lifecycleState = doc.getLifeCycleState();
190        } catch (LifeCycleException e) {
191            lifecycleState = null;
192        }
193        String type = doc.getType().getName();
194        return getSaveOptions(versionable, lifecycleState, type);
195    }
196
197    protected List<VersioningOption> getSaveOptions(boolean versionable, String lifecycleState, String type) {
198        if (!versionable) {
199            return Arrays.asList(NONE);
200        }
201        if (lifecycleState == null) {
202            return Arrays.asList(NONE);
203        }
204        SaveOptionsDescriptor option = null;
205        if (versioningRules != null) {
206            VersioningRuleDescriptor saveOption = versioningRules.get(type);
207            if (saveOption != null) {
208                option = saveOption.getOptions().get(lifecycleState);
209                if (option == null) {
210                    // try on any life cycle state
211                    option = saveOption.getOptions().get("*");
212                }
213            }
214        }
215        if (option == null && defaultVersioningRule != null) {
216            option = defaultVersioningRule.getOptions().get(lifecycleState);
217            if (option == null) {
218                // try on any life cycle state
219                option = defaultVersioningRule.getOptions().get("*");
220            }
221        }
222        if (option != null) {
223            return option.getVersioningOptionList();
224        }
225        if (PROJECT_STATE.equals(lifecycleState) || APPROVED_STATE.equals(lifecycleState)
226                || OBSOLETE_STATE.equals(lifecycleState)) {
227            return Arrays.asList(NONE, MINOR, MAJOR);
228        }
229        if (FILE_TYPE.equals(type) || NOTE_TYPE.equals(type)) {
230            return Arrays.asList(NONE, MINOR, MAJOR);
231        }
232        return Arrays.asList(NONE);
233    }
234
235    protected VersioningOption validateOption(Document doc, VersioningOption option) {
236        List<VersioningOption> options = getSaveOptions(doc);
237        if (!options.contains(option)) {
238            option = options.isEmpty() ? NONE : options.get(0);
239        }
240        return option;
241    }
242
243    @Override
244    public boolean isPreSaveDoingCheckOut(Document doc, boolean isDirty, VersioningOption option,
245            Map<String, Serializable> options) {
246        boolean disableAutoCheckOut = Boolean.TRUE.equals(options.get(VersioningService.DISABLE_AUTO_CHECKOUT));
247        return !doc.isCheckedOut() && isDirty && !disableAutoCheckOut;
248    }
249
250    @Override
251    public VersioningOption doPreSave(Document doc, boolean isDirty, VersioningOption option, String checkinComment,
252            Map<String, Serializable> options) {
253        option = validateOption(doc, option);
254        if (isPreSaveDoingCheckOut(doc, isDirty, option, options)) {
255            doCheckOut(doc);
256            followTransitionByOption(doc, option);
257        }
258        // transition follow shouldn't change what postSave options will be
259        return option;
260    }
261
262    protected void followTransitionByOption(Document doc, VersioningOption option) {
263        String lifecycleState = doc.getLifeCycleState();
264        if (APPROVED_STATE.equals(lifecycleState) || OBSOLETE_STATE.equals(lifecycleState)) {
265            doc.followTransition(BACK_TO_PROJECT_TRANSITION);
266        }
267    }
268
269    @Override
270    public boolean isPostSaveDoingCheckIn(Document doc, VersioningOption option, Map<String, Serializable> options) {
271        // option = validateOption(doc, option); // validated before
272        return doc.isCheckedOut() && option != NONE;
273    }
274
275    @Override
276    public Document doPostSave(Document doc, VersioningOption option, String checkinComment,
277            Map<String, Serializable> options) {
278        if (isPostSaveDoingCheckIn(doc, option, options)) {
279            incrementByOption(doc, option);
280            return doc.checkIn(null, checkinComment); // auto-label
281        }
282        return null;
283    }
284
285    @Override
286    public Document doCheckIn(Document doc, VersioningOption option, String checkinComment) {
287        if (option != VersioningOption.NONE) {
288            incrementByOption(doc, option == MAJOR ? MAJOR : MINOR);
289        }
290        return doc.checkIn(null, checkinComment); // auto-label
291    }
292
293    @Override
294    public void doCheckOut(Document doc) {
295        Document base = doc.getBaseVersion();
296        doc.checkOut();
297        // set version number to that of the latest version
298        if (base.isLatestVersion()) {
299            // nothing to do, already at proper version
300        } else {
301            // this doc was restored from a non-latest version, find the latest one
302            Document last = doc.getLastVersion();
303            if (last != null) {
304                try {
305                    setVersion(doc, getMajor(last), getMinor(last));
306                } catch (PropertyNotFoundException e) {
307                    // ignore
308                }
309            }
310        }
311    }
312
313    @Override
314    public Map<String, VersioningRuleDescriptor> getVersioningRules() {
315        return versioningRules;
316    }
317
318    @Override
319    public void setVersioningRules(Map<String, VersioningRuleDescriptor> versioningRules) {
320        this.versioningRules = versioningRules;
321    }
322
323    @Override
324    public void setDefaultVersioningRule(DefaultVersioningRuleDescriptor defaultVersioningRule) {
325        this.defaultVersioningRule = defaultVersioningRule;
326    }
327
328}