/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.sling.nosql.mongodb.resourceprovider.impl; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.regex.Pattern; import org.apache.sling.api.resource.LoginException; import org.apache.sling.api.resource.ResourceUtil; import org.apache.sling.nosql.generic.adapter.AbstractNoSqlAdapter; import org.apache.sling.nosql.generic.adapter.MultiValueMode; import org.apache.sling.nosql.generic.adapter.NoSqlData; import org.bson.Document; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mongodb.DuplicateKeyException; import com.mongodb.MongoClient; import com.mongodb.MongoException; import com.mongodb.client.FindIterable; import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoCursor; import com.mongodb.client.MongoDatabase; import com.mongodb.client.model.Filters; import com.mongodb.client.model.IndexOptions; import com.mongodb.client.model.UpdateOptions; import com.mongodb.client.result.DeleteResult; import com.mongodb.client.result.UpdateResult; /** * {@link org.apache.sling.nosql.generic.adapter.NoSqlAdapter} implementation for MongoDB. */ public final class MongoDBNoSqlAdapter extends AbstractNoSqlAdapter { private static final String PN_PATH = "_id"; private static final String PN_PARENT_PATH = "parentPath"; private static final String PN_DATA = "data"; private final MongoCollection<Document> collection; private static final Logger log = LoggerFactory.getLogger(MongoDBNoSqlAdapter.class); /** * @param mongoClient MongoDB client * @param database MongoDB database * @param collection MongoDB collection */ public MongoDBNoSqlAdapter(MongoClient mongoClient, String database, String collection) { MongoDatabase db = mongoClient.getDatabase(database); this.collection = db.getCollection(collection); } @Override public NoSqlData get(String path) { Document envelope = collection.find(Filters.eq(PN_PATH, path)).first(); if (envelope == null) { return null; } else { return new NoSqlData(path, envelope.get(PN_DATA, Document.class), MultiValueMode.LISTS); } } @Override public Iterator<NoSqlData> getChildren(String parentPath) { List<NoSqlData> children = new ArrayList<>(); FindIterable<Document> result = collection.find(Filters.eq(PN_PARENT_PATH, parentPath)); try (MongoCursor<Document> envelopes = result.iterator()) { while (envelopes.hasNext()) { Document envelope = envelopes.next(); String path = envelope.get(PN_PATH, String.class); Document data = envelope.get(PN_DATA, Document.class); children.add(new NoSqlData(path, data, MultiValueMode.LISTS)); } } return children.iterator(); } @Override public boolean store(NoSqlData data) { Document envelope = new Document(); envelope.put(PN_PATH, data.getPath()); envelope.put(PN_DATA, new Document(data.getProperties(MultiValueMode.LISTS))); // for list-children query efficiency store parent path as well String parentPath = ResourceUtil.getParent(data.getPath()); if (parentPath != null) { envelope.put(PN_PARENT_PATH, parentPath); } UpdateResult result = collection.replaceOne(Filters.eq(PN_PATH, data.getPath()), envelope, new UpdateOptions().upsert(true)); // return true if a new entry was inserted, false if an existing was replaced return (result.getMatchedCount() == 0); } @Override public boolean deleteRecursive(String path) { Pattern descendantsAndSelf = Pattern.compile("^" + Pattern.quote(path) + "(/.+)?$"); DeleteResult result = collection.deleteMany(Filters.regex(PN_PATH, descendantsAndSelf)); // return true if any document was deleted return result.getDeletedCount() > 0; } @Override public void checkConnection() throws LoginException { // the query is not relevant, just the successful round-trip try { collection.find(Filters.eq(PN_PATH, "/")).first(); } catch (MongoException e) { throw new LoginException(e); } } @Override public void createIndexDefinitions() { // create index on parent path field (if it does not exist yet) try { Document parenPathtIndex = new Document(PN_PARENT_PATH, 1); this.collection.createIndex(parenPathtIndex); } catch (DuplicateKeyException ex) { // index already exists, ignore } catch (Throwable ex) { log.error("Unable to create index on " + PN_PARENT_PATH + ": " + ex.getMessage(), ex); } // create unique index on path field (if it does not exist yet) try { Document pathIndex = new Document(PN_PATH, 1); IndexOptions idxOptions = new IndexOptions(); idxOptions.unique(true); this.collection.createIndex(pathIndex, idxOptions); } catch (DuplicateKeyException ex) { // index already exists, ignore } catch (Throwable ex) { log.error("Unable to create unique index on " + PN_PATH + ": " + ex.getMessage(), ex); } } }