001/*
002 * (C) Copyright 2017 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 *     Kevin Leturc
018 */
019package org.nuxeo.ecm.core.mongodb.seqgen;
020
021import static com.mongodb.client.model.Filters.and;
022import static com.mongodb.client.model.Filters.eq;
023import static com.mongodb.client.model.Filters.gte;
024import static com.mongodb.client.model.Filters.not;
025
026import org.apache.commons.logging.Log;
027import org.apache.commons.logging.LogFactory;
028import org.bson.Document;
029import org.bson.conversions.Bson;
030import org.nuxeo.ecm.core.api.NuxeoException;
031import org.nuxeo.ecm.core.uidgen.AbstractUIDSequencer;
032import org.nuxeo.ecm.core.uidgen.UIDSequencer;
033import org.nuxeo.runtime.api.Framework;
034import org.nuxeo.runtime.mongodb.MongoDBConnectionService;
035import org.nuxeo.runtime.mongodb.MongoDBSerializationHelper;
036import org.nuxeo.runtime.services.config.ConfigurationService;
037
038import com.mongodb.MongoWriteException;
039import com.mongodb.client.MongoCollection;
040import com.mongodb.client.MongoDatabase;
041import com.mongodb.client.model.FindOneAndUpdateOptions;
042import com.mongodb.client.model.ReturnDocument;
043import com.mongodb.client.model.UpdateOptions;
044import com.mongodb.client.model.Updates;
045
046/**
047 * MongoDB implementation of {@link UIDSequencer}.
048 * <p>
049 * We use MongoDB upsert feature to provide a sequencer.
050 *
051 * @since 9.1
052 */
053public class MongoDBUIDSequencer extends AbstractUIDSequencer {
054
055    private static final Log log = LogFactory.getLog(MongoDBUIDSequencer.class);
056
057    public static final String SEQUENCE_DATABASE_ID = "sequence";
058
059    public static final String COLLECTION_NAME_PROPERTY = "nuxeo.mongodb.seqgen.collection.name";
060
061    public static final String DEFAULT_COLLECTION_NAME = "sequence";
062
063    public static final Long ONE = Long.valueOf(1L);
064
065    public static final String SEQUENCE_VALUE_FIELD = "sequence";
066
067    protected MongoCollection<Document> coll;
068
069    @Override
070    public void init() {
071        getSequencerCollection();
072    }
073
074    @Override
075    public void initSequence(String key, long id) {
076        try {
077            Bson filter = and(eq(MongoDBSerializationHelper.MONGODB_ID, key), not(gte(SEQUENCE_VALUE_FIELD, id)));
078            Document sequence = new Document();
079            sequence.put(MongoDBSerializationHelper.MONGODB_ID, key);
080            sequence.put(SEQUENCE_VALUE_FIELD, id);
081            getSequencerCollection().replaceOne(filter, sequence, new UpdateOptions().upsert(true));
082        } catch (MongoWriteException e) {
083            throw new NuxeoException("Failed to update the sequence '" + key + "' with value " + id, e);
084        }
085    }
086
087    public MongoCollection<Document> getSequencerCollection() {
088        if (coll == null) {
089            // Get collection name
090            ConfigurationService configurationService = Framework.getService(ConfigurationService.class);
091            String collName = configurationService.getProperty(COLLECTION_NAME_PROPERTY, DEFAULT_COLLECTION_NAME);
092            // Get a connection to MongoDB
093            MongoDBConnectionService mongoService = Framework.getService(MongoDBConnectionService.class);
094            // Get database
095            MongoDatabase database = mongoService.getDatabase(SEQUENCE_DATABASE_ID);
096            // Get collection
097            coll = database.getCollection(collName);
098        }
099        return coll;
100    }
101
102    @Override
103    public long getNextLong(String key) {
104        FindOneAndUpdateOptions options = new FindOneAndUpdateOptions().returnDocument(ReturnDocument.AFTER);
105        Bson filter = eq(MongoDBSerializationHelper.MONGODB_ID, key);
106        Bson update = Updates.inc(SEQUENCE_VALUE_FIELD, ONE);
107        Document sequence = getSequencerCollection().findOneAndUpdate(filter, update, options);
108        // If sequence is null, we need to create it
109        if (sequence == null) {
110            try {
111                sequence = new Document();
112                sequence.put(MongoDBSerializationHelper.MONGODB_ID, key);
113                sequence.put(SEQUENCE_VALUE_FIELD, ONE);
114                getSequencerCollection().insertOne(sequence);
115            } catch (MongoWriteException e) {
116                // There was a race condition - just re-run getNextLong
117                if (log.isTraceEnabled()) {
118                    log.trace("There was a race condition during '" + key + "' sequence insertion", e);
119                }
120                return getNextLong(key);
121            }
122        }
123        return ((Long) MongoDBSerializationHelper.bsonToFieldMap(sequence).get(SEQUENCE_VALUE_FIELD)).longValue();
124    }
125
126    @Override
127    public void dispose() {
128        if (coll != null) {
129            coll = null;
130        }
131    }
132
133}