/* * ToroDB * Copyright © 2014 8Kdata Technology (www.8kdata.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.torodb.mongodb.repl; import com.eightkdata.mongowp.Status; import com.eightkdata.mongowp.bson.BsonDocument; import com.eightkdata.mongowp.server.api.Command; import com.eightkdata.mongowp.server.api.oplog.DbCmdOplogOperation; import com.eightkdata.mongowp.server.api.oplog.DbOplogOperation; import com.eightkdata.mongowp.server.api.oplog.DeleteOplogOperation; import com.eightkdata.mongowp.server.api.oplog.InsertOplogOperation; import com.eightkdata.mongowp.server.api.oplog.NoopOplogOperation; import com.eightkdata.mongowp.server.api.oplog.OplogOperation; import com.eightkdata.mongowp.server.api.oplog.OplogOperationVisitor; import com.eightkdata.mongowp.server.api.oplog.UpdateOplogOperation; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.torodb.core.exceptions.SystemException; import com.torodb.mongodb.commands.pojos.index.IndexOptions; import com.torodb.mongodb.commands.signatures.admin.CreateCollectionCommand; import com.torodb.mongodb.commands.signatures.admin.CreateIndexesCommand; import com.torodb.mongodb.commands.signatures.admin.DropCollectionCommand; import com.torodb.mongodb.commands.signatures.admin.DropIndexesCommand; import com.torodb.mongodb.commands.signatures.admin.RenameCollectionCommand; import com.torodb.mongodb.commands.signatures.general.DeleteCommand; import com.torodb.mongodb.commands.signatures.general.InsertCommand; import com.torodb.mongodb.commands.signatures.general.UpdateCommand; import com.torodb.mongodb.repl.oplogreplier.fetcher.FilteredOplogFetcher; import com.torodb.mongodb.repl.oplogreplier.fetcher.OplogFetcher; import com.torodb.mongodb.utils.IndexPredicate; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.jooq.lambda.fi.util.function.CheckedFunction; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.function.BiPredicate; import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.Nonnull; import javax.annotation.Nullable; public class ReplicationFilters { private static final Logger LOGGER = LogManager.getLogger(ReplicationFilters.class); private final ImmutableMap<Pattern, ImmutableMap<Pattern, ImmutableList<IndexPattern>>> whitelist; private final ImmutableMap<Pattern, ImmutableMap<Pattern, ImmutableList<IndexPattern>>> blacklist; private final DatabasePredicate databasePredicate = new DatabasePredicate(); private final CollectionPredicate collectionPredicate = new CollectionPredicate(); private final IndexPredicateImpl indexPredicate = new IndexPredicateImpl(); private final OplogOperationPredicate oplogOperationPredicate = new OplogOperationPredicate(); public ReplicationFilters( ImmutableMap<Pattern, ImmutableMap<Pattern, ImmutableList<IndexPattern>>> whitelist, ImmutableMap<Pattern, ImmutableMap<Pattern, ImmutableList<IndexPattern>>> blacklist) { super(); this.whitelist = whitelist; this.blacklist = blacklist; } public Predicate<String> getDatabasePredicate() { return databasePredicate; } public BiPredicate<String, String> getCollectionPredicate() { return collectionPredicate; } public IndexPredicate getIndexPredicate() { return indexPredicate; } public Predicate<OplogOperation> getOperationPredicate() { return oplogOperationPredicate; } public OplogFetcher filterOplogFetcher(OplogFetcher originalFetcher) { return new FilteredOplogFetcher(oplogOperationPredicate, originalFetcher); } @FunctionalInterface public interface ResultFilter { public <R> Status<R> filter(Status<R> result); } @SuppressWarnings("checkstyle:LineLength") private boolean databaseWhiteFilter(String database) { if (whitelist.isEmpty()) { return true; } for (Map.Entry<Pattern, ImmutableMap<Pattern, ImmutableList<IndexPattern>>> filterEntry : whitelist .entrySet()) { Matcher databaseMatcher = filterEntry.getKey().matcher(database); if (databaseMatcher.matches()) { return true; } } return false; } @SuppressWarnings("checkstyle:LineLength") private boolean databaseBlackFilter(String database) { if (blacklist.isEmpty()) { return true; } for (Map.Entry<Pattern, ImmutableMap<Pattern, ImmutableList<IndexPattern>>> filterEntry : blacklist .entrySet()) { Matcher databaseMatcher = filterEntry.getKey().matcher(database); if (databaseMatcher.matches()) { if (filterEntry.getValue().isEmpty()) { return false; } } } return true; } @SuppressWarnings("checkstyle:LineLength") private boolean collectionWhiteFilter(String database, String collection) { if (whitelist.isEmpty()) { return true; } for (Map.Entry<Pattern, ImmutableMap<Pattern, ImmutableList<IndexPattern>>> filterEntry : whitelist .entrySet()) { Matcher databaseMatcher = filterEntry.getKey().matcher(database); if (databaseMatcher.matches()) { if (filterEntry.getValue().isEmpty()) { return true; } for (Map.Entry<Pattern, ImmutableList<IndexPattern>> collectionPattern : filterEntry .getValue().entrySet()) { Matcher collectionMatcher = collectionPattern.getKey().matcher(collection); if (collectionMatcher.matches()) { return true; } } } } return false; } @SuppressWarnings("checkstyle:LineLength") private boolean collectionBlackFilter(String database, String collection) { if (blacklist.isEmpty()) { return true; } for (Map.Entry<Pattern, ImmutableMap<Pattern, ImmutableList<IndexPattern>>> filterEntry : blacklist .entrySet()) { Matcher databaseMatcher = filterEntry.getKey().matcher(database); if (databaseMatcher.matches()) { if (filterEntry.getValue().isEmpty()) { return false; } for (Map.Entry<Pattern, ImmutableList<IndexPattern>> collectionPattern : filterEntry .getValue().entrySet()) { if (collectionPattern.getValue().isEmpty()) { Matcher collectionMatcher = collectionPattern.getKey().matcher(collection); if (collectionMatcher.matches()) { return false; } } } } } return true; } @SuppressWarnings("checkstyle:LineLength") private boolean indexWhiteFilter(String database, String collection, String indexName, boolean unique, List<IndexOptions.Key> keys) { if (whitelist.isEmpty()) { return true; } for (Map.Entry<Pattern, ImmutableMap<Pattern, ImmutableList<IndexPattern>>> filterEntry : whitelist .entrySet()) { Matcher databaseMatcher = filterEntry.getKey().matcher(database); if (databaseMatcher.matches()) { if (filterEntry.getValue().isEmpty()) { return true; } for (Map.Entry<Pattern, ImmutableList<IndexPattern>> collectionPattern : filterEntry .getValue().entrySet()) { Matcher collectionMatcher = collectionPattern.getKey().matcher(collection); if (collectionMatcher.matches()) { if (collectionPattern.getValue().isEmpty()) { return true; } for (IndexPattern indexPattern : collectionPattern.getValue()) { if (indexPattern.match(indexName, unique, keys)) { return true; } } } } } } return false; } @SuppressWarnings("checkstyle:LineLength") private boolean indexBlackFilter(String database, String collection, String indexName, boolean unique, List<IndexOptions.Key> keys) { if (blacklist.isEmpty()) { return true; } for (Map.Entry<Pattern, ImmutableMap<Pattern, ImmutableList<IndexPattern>>> filterEntry : blacklist .entrySet()) { Matcher databaseMatcher = filterEntry.getKey().matcher(database); if (databaseMatcher.matches()) { if (filterEntry.getValue().isEmpty()) { return false; } for (Map.Entry<Pattern, ImmutableList<IndexPattern>> collectionPattern : filterEntry .getValue().entrySet()) { Matcher collectionMatcher = collectionPattern.getKey().matcher(collection); if (collectionMatcher.matches()) { if (collectionPattern.getValue().isEmpty()) { return false; } for (IndexPattern indexPattern : collectionPattern.getValue()) { if (indexPattern.match(indexName, unique, keys)) { return false; } } } } } } return true; } private class DatabasePredicate implements Predicate<String> { @Override public boolean test(String database) { return databaseWhiteFilter(database) && databaseBlackFilter(database); } } private class CollectionPredicate implements BiPredicate<String, String> { @Override public boolean test(String database, String collection) { return collectionWhiteFilter(database, collection) && collectionBlackFilter(database, collection); } } public class IndexPredicateImpl implements IndexPredicate { @Override public boolean test(String database, String collection, String indexName, boolean unique, List<IndexOptions.Key> keys) { return indexWhiteFilter(database, collection, indexName, unique, keys) && indexBlackFilter( database, collection, indexName, unique, keys); } } @SuppressWarnings("checkstyle:LineLength") private static final ImmutableMap<Command<?, ?>, CheckedFunction<BsonDocument, String>> collectionCommands = ImmutableMap.<Command<?, ?>, CheckedFunction<BsonDocument, String>>builder() .put(CreateCollectionCommand.INSTANCE, d -> CreateCollectionCommand.INSTANCE .unmarshallArg(d).getCollection()) .put(CreateIndexesCommand.INSTANCE, d -> CreateIndexesCommand.INSTANCE.unmarshallArg(d) .getCollection()) .put(DropIndexesCommand.INSTANCE, d -> DropIndexesCommand.INSTANCE.unmarshallArg(d) .getCollection()) .put(DropCollectionCommand.INSTANCE, d -> DropCollectionCommand.INSTANCE .unmarshallArg(d).getCollection()) .put(RenameCollectionCommand.INSTANCE, d -> RenameCollectionCommand.INSTANCE .unmarshallArg(d).getFromCollection()) .put(DeleteCommand.INSTANCE, d -> DeleteCommand.INSTANCE.unmarshallArg(d) .getCollection()) .put(InsertCommand.INSTANCE, d -> InsertCommand.INSTANCE.unmarshallArg(d) .getCollection()) .put(UpdateCommand.INSTANCE, d -> UpdateCommand.INSTANCE.unmarshallArg(d) .getCollection()) .build(); private class OplogOperationPredicate implements OplogOperationVisitor<Boolean, Void>, Predicate<OplogOperation> { @Override public Boolean visit(DbCmdOplogOperation op, Void arg) { if (op.getCommandName().isPresent()) { String commandName = op.getCommandName().get(); if (collectionCommands.containsKey(commandName)) { try { assert op.getRequest() != null; String collection = collectionCommands.get(commandName) .apply(op.getRequest()); return testCollection(commandName, op.getDatabase(), collection); } catch (Throwable e) { throw new SystemException("Error while parsing argument for command " + op .getCommandName(), e); } } return testDatabase(commandName, op.getDatabase()); } return testDatabase("unknown", op.getDatabase()); } @Override public Boolean visit(DbOplogOperation op, Void arg) { return testDatabase("unknown", op.getDatabase()); } @Override public Boolean visit(DeleteOplogOperation op, Void arg) { return collectionPredicate.test(op.getDatabase(), op.getCollection()); } @Override public Boolean visit(InsertOplogOperation op, Void arg) { return collectionPredicate.test(op.getDatabase(), op.getCollection()); } @Override public Boolean visit(NoopOplogOperation op, Void arg) { return true; } @Override public Boolean visit(UpdateOplogOperation op, Void arg) { return collectionPredicate.test(op.getDatabase(), op.getCollection()); } private boolean testDatabase(String commandName, String database) { if (databasePredicate.test(database)) { return true; } LOGGER.info("Skipping operation {} for filtered database {}.", commandName, database); return false; } private boolean testCollection(String commandName, String database, String collection) { if (collectionPredicate.test(database, collection)) { return true; } LOGGER.info("Skipping operation {} for filtered collection {}.{}.", commandName, database, collection); return false; } @Override public boolean test(OplogOperation t) { return t.accept(this, null); } } public static class IndexPattern { private final Pattern name; private final Boolean unique; private final ImmutableList<IndexFieldPattern> fieldsPattern; public IndexPattern(@Nonnull Pattern name, @Nullable Boolean unique, @Nonnull ImmutableList<IndexFieldPattern> fieldsPattern) { super(); this.name = name; this.unique = unique; this.fieldsPattern = fieldsPattern; } public boolean match(String name, boolean unique, List<IndexOptions.Key> fields) { if (this.name.matcher(name).matches() && (this.unique == null || this.unique.booleanValue() == unique) && (this.fieldsPattern.isEmpty() || this.fieldsPattern.size() == fields.size())) { if (this.fieldsPattern.isEmpty()) { return true; } Iterator<IndexOptions.Key> fieldIterator = fields.iterator(); Iterator<IndexFieldPattern> fieldPatternIterator = fieldsPattern.iterator(); while (fieldPatternIterator.hasNext() && fieldIterator.hasNext()) { IndexFieldPattern fieldPattern = fieldPatternIterator.next(); IndexOptions.Key field = fieldIterator.next(); if (!fieldPattern.getType().matcher(field.getType().getName()).matches() || fieldPattern .getKeys().size() != field.getKeys().size()) { return false; } Iterator<Pattern> fieldReferencePatternIterator = fieldPattern.getKeys().iterator(); Iterator<String> fieldReferenceIterator = field.getKeys().iterator(); while (fieldReferencePatternIterator.hasNext() && fieldReferenceIterator.hasNext()) { Pattern fieldReferencePattern = fieldReferencePatternIterator.next(); String fieldReference = fieldReferenceIterator.next(); if (!fieldReferencePattern.matcher(fieldReference).matches()) { return false; } } } return true; } return false; } public static class Builder { private final Pattern name; private final Boolean unique; private final List<IndexFieldPattern> fieldsPattern = new ArrayList<>(); public Builder(@Nonnull Pattern name, @Nullable Boolean unique) { this.name = name; this.unique = unique; } public Builder addFieldPattern(ImmutableList<Pattern> fieldReferencePattern, Pattern typePattern) { fieldsPattern.add(new IndexFieldPattern(fieldReferencePattern, typePattern)); return this; } public IndexPattern build() { return new IndexPattern(name, unique, ImmutableList.copyOf(fieldsPattern)); } } } public static class IndexFieldPattern { private final List<Pattern> keys; private final Pattern type; public IndexFieldPattern(List<Pattern> keys, Pattern type) { super(); this.keys = keys; this.type = type; } public List<Pattern> getKeys() { return keys; } public Pattern getType() { return type; } } }