AclIndexOperations.java
/*
* Copyright 2022 the original author or authors.
*
* Licensed 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.bremersee.acl.spring.data.mongodb;
import static org.springframework.core.annotation.AnnotationUtils.findAnnotation;
import static org.springframework.util.ObjectUtils.isEmpty;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.bremersee.acl.Ace;
import org.bremersee.acl.Acl;
import org.bremersee.acl.annotation.AclHolder;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.index.Index;
import org.springframework.data.mongodb.core.index.IndexInfo;
import org.springframework.data.mongodb.core.index.IndexOperations;
import org.springframework.util.Assert;
/**
* The acl index operations.
*
* @author Christian Bremer
*/
public class AclIndexOperations {
private final MongoOperations mongoOperations;
/**
* Instantiates a new acl index operations.
*
* @param mongoOperations the mongo operations
*/
public AclIndexOperations(MongoOperations mongoOperations) {
Assert.notNull(mongoOperations, "Mongo operations must be present.");
this.mongoOperations = mongoOperations;
}
/**
* Gets acl index info.
*
* @param entityClass the entity class
* @return the acl index info
*/
public List<IndexInfo> getAclIndexInfo(Class<?> entityClass) {
Assert.notNull(entityClass, "Entity class must be present.");
String aclPath = getAclPath(entityClass);
return getAclIndexInfo(entityClass, aclPath);
}
/**
* Gets acl index info.
*
* @param entityClass the entity class
* @param aclPath the acl path
* @return the acl index info
*/
public List<IndexInfo> getAclIndexInfo(Class<?> entityClass, String aclPath) {
Assert.notNull(entityClass, "Entity class must be present.");
return getAclIndexInfo(mongoOperations.indexOps(entityClass), aclPath);
}
/**
* Gets acl index info.
*
* @param collectionName the collection name
* @param aclPath the acl path
* @return the acl index info
*/
public List<IndexInfo> getAclIndexInfo(String collectionName, String aclPath) {
Assert.hasLength(collectionName, "Collection name must be present.");
return getAclIndexInfo(mongoOperations.indexOps(collectionName), aclPath);
}
private List<IndexInfo> getAclIndexInfo(IndexOperations indexOps, String aclPath) {
String validAclPath = isEmpty(aclPath) ? "" : aclPath.trim() + ".";
// Example: acl.(owner|(entries.(\.*).(guest|users|roles|groups)))(.*)
String regex = String.format(
"%s(%s|(%s.(.*).(%s|%s|%s|%s)))(.*)",
validAclPath, Acl.OWNER, Acl.ENTRIES, Ace.GUEST, Ace.USERS, Ace.ROLES, Ace.GROUPS);
Pattern pattern = Pattern.compile(regex);
return indexOps.getIndexInfo()
.stream()
.filter(indexInfo -> pattern.matcher(indexInfo.getName()).matches())
.collect(Collectors.toList());
}
/**
* Ensure acl indexes.
*
* @param entityClass the entity class
* @param possiblePermissions the possible permissions
* @param dropIndexesOfOtherPermissions the drop indexes of other permissions
*/
public void ensureAclIndexes(
Class<?> entityClass,
Collection<String> possiblePermissions,
boolean dropIndexesOfOtherPermissions) {
Assert.notNull(entityClass, "Entity class must be present.");
String aclPath = getAclPath(entityClass);
ensureAclIndexes(entityClass, aclPath, possiblePermissions, dropIndexesOfOtherPermissions);
}
/**
* Ensure acl indexes.
*
* @param entityClass the entity class
* @param aclPath the acl path
* @param possiblePermissions the possible permissions
* @param dropIndexesOfOtherPermissions the drop indexes of other permissions
*/
public void ensureAclIndexes(
Class<?> entityClass,
String aclPath,
Collection<String> possiblePermissions,
boolean dropIndexesOfOtherPermissions) {
Assert.notNull(entityClass, "Entity class must be present.");
ensureAclIndexes(
mongoOperations.indexOps(entityClass),
aclPath,
possiblePermissions,
dropIndexesOfOtherPermissions);
}
/**
* Ensure acl indexes.
*
* @param collectionName the collection name
* @param aclPath the acl path
* @param possiblePermissions the possible permissions
* @param dropIndexesOfOtherPermissions the drop indexes of other permissions
*/
public void ensureAclIndexes(
String collectionName,
String aclPath,
Collection<String> possiblePermissions,
boolean dropIndexesOfOtherPermissions) {
Assert.hasLength(collectionName, "Collection name must be present.");
ensureAclIndexes(
mongoOperations.indexOps(collectionName.trim()),
aclPath,
possiblePermissions,
dropIndexesOfOtherPermissions);
}
private void ensureAclIndexes(
IndexOperations indexOps,
String aclPath,
Collection<String> possiblePermissions,
boolean dropIndexesOfOtherPermissions) {
if (dropIndexesOfOtherPermissions) {
dropUnusedAclIndexes(indexOps, aclPath, possiblePermissions);
}
String validAclPath = isEmpty(aclPath) ? "" : aclPath.trim();
indexOps.ensureIndex(new Index()
.on(path(validAclPath, Acl.OWNER), Direction.ASC));
if (!isEmpty(possiblePermissions)) {
List.of(Ace.GUEST, Ace.USERS, Ace.ROLES, Ace.GROUPS)
.forEach(field -> Set.copyOf(possiblePermissions)
.forEach(permission -> indexOps
.ensureIndex(new Index()
.on(path(validAclPath, Acl.ENTRIES, permission, field),
Direction.ASC))));
}
}
private void dropUnusedAclIndexes(
IndexOperations indexOps,
String aclPath,
Collection<String> possiblePermissions) {
Set<String> newPermissions = isEmpty(possiblePermissions)
? Set.of()
: Set.copyOf(possiblePermissions);
String validAclPath = isEmpty(aclPath) ? "" : aclPath.trim() + ".";
// Example: acl.entries.(\.*).(guest|users|roles|groups)(.*)
String regex = String.format(
"%s%s.(.*).(%s|%s|%s|%s)(.*)",
validAclPath, Acl.ENTRIES, Ace.GUEST, Ace.USERS, Ace.ROLES, Ace.GROUPS);
Pattern pattern = Pattern.compile(regex);
getAclIndexInfo(indexOps, aclPath)
.stream()
.filter(indexInfo -> {
Matcher matcher = pattern.matcher(indexInfo.getName());
return matcher.matches() && !newPermissions.contains(matcher.group(1));
})
.forEach(indexInfo -> indexOps.dropIndex(indexInfo.getName()));
}
private String getAclPath(Class<?> entityClass) {
return Optional
.ofNullable(findAnnotation(entityClass, AclHolder.class))
.map(AclHolder::path)
.orElseThrow(() -> new IllegalArgumentException(String
.format(
"Entity class %s must be annotated with %s.",
entityClass.getSimpleName(), AclHolder.class.getSimpleName())));
}
private String path(String... pathSegments) {
String aclPath = String.join(".", pathSegments);
return aclPath.startsWith(".") ? aclPath.substring(1) : aclPath;
}
}