Skip to content

Commit

Permalink
Add SpEL expression parsing for database names (#103)
Browse files Browse the repository at this point in the history
  • Loading branch information
mpv1989 committed Jul 25, 2018
1 parent 176f5da commit 7224786
Show file tree
Hide file tree
Showing 8 changed files with 313 additions and 38 deletions.
3 changes: 3 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
- added convenience method `ArangoOperations#query(String, Map<String, Object>, Class)`
- added support for non-String `@Id`s (issue #79)
- added convenience method `AbstractArangoConfiguration#customConverters()` to add custom converters
- added SpEL expression parsing for database names

SpEL expressions can now be used within `AbstractArangoConfiguration#database()`. This allows Multi-tenancy on database level.

### Changed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,19 @@
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.expression.BeanFactoryAccessor;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.data.domain.Persistable;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.expression.Expression;
import org.springframework.expression.ParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

import com.arangodb.ArangoCollection;
import com.arangodb.ArangoCursor;
Expand Down Expand Up @@ -81,15 +90,20 @@
* @author Christian Lechner
* @author Reşat SABIQ
*/
public class ArangoTemplate implements ArangoOperations, CollectionCallback {
public class ArangoTemplate implements ArangoOperations, CollectionCallback, ApplicationContextAware {

private static final SpelExpressionParser PARSER = new SpelExpressionParser();

private volatile ArangoDBVersion version;
private final PersistenceExceptionTranslator exceptionTranslator;
private final ArangoConverter converter;
private final ArangoDB arango;
private volatile ArangoDatabase database;
private final String databaseName;
private final Map<String, ArangoCollection> collectionCache;
private final Expression databaseExpression;
private final Map<String, ArangoDatabase> databaseCache;
private final Map<CollectionCacheKey, CollectionCacheValue> collectionCache;

private final StandardEvaluationContext context;

public ArangoTemplate(final ArangoDB arango, final String database) {
this(arango, database, null);
Expand All @@ -105,32 +119,26 @@ public ArangoTemplate(final ArangoDB arango, final String database, final Arango
this.arango = arango._setCursorInitializer(
new com.arangodb.springframework.core.template.ArangoCursorInitializer(converter));
this.databaseName = database;
this.databaseExpression = PARSER.parseExpression(databaseName, ParserContext.TEMPLATE_EXPRESSION);
this.converter = converter;
this.exceptionTranslator = exceptionTranslator;
this.context = new StandardEvaluationContext();
// set concurrency level to 1 as writes are very rare compared to reads
collectionCache = new ConcurrentHashMap<>(8, 0.9f, 1);
databaseCache = new ConcurrentHashMap<>(8, 0.9f, 1);
version = null;
}

private ArangoDatabase db() {
// guard against NPE because database can be set to null by dropDatabase() by another thread
ArangoDatabase db = database;
if (db != null) {
return db;
}
// make sure the database is only created once
synchronized (this) {
db = database;
if (db != null) {
return db;
}
db = arango.db(databaseName);
final String key = databaseExpression != null ? databaseExpression.getValue(context, String.class)
: databaseName;
return databaseCache.computeIfAbsent(key, name -> {
final ArangoDatabase db = arango.db(name);
if (!db.exists()) {
db.create();
}
database = db;
return db;
}
});
}

private DataAccessException translateExceptionIfPossible(final RuntimeException exception) {
Expand All @@ -157,16 +165,23 @@ private ArangoCollection _collection(
final ArangoPersistentEntity<?> persistentEntity,
final CollectionCreateOptions options) {

return collectionCache.computeIfAbsent(name, collName -> {
final ArangoCollection collection = db().collection(collName);
if (!collection.exists()) {
collection.create(options);
}
if (persistentEntity != null) {
ensureCollectionIndexes(collection(collection), persistentEntity);
}
return collection;
});
final ArangoDatabase db = db();
final Class<?> entityClass = persistentEntity != null ? persistentEntity.getType() : null;
final CollectionCacheValue value = collectionCache.computeIfAbsent(new CollectionCacheKey(db.name(), name),
key -> {
final ArangoCollection collection = db.collection(name);
if (!collection.exists()) {
collection.create(options);
}
return new CollectionCacheValue(collection);
});
final Collection<Class<?>> entities = value.getEntities();
final ArangoCollection collection = value.getCollection();
if (persistentEntity != null && !entities.contains(entityClass)) {
value.addEntityClass(entityClass);
ensureCollectionIndexes(collection(collection), persistentEntity);
}
return collection;
}

private static void ensureCollectionIndexes(
Expand Down Expand Up @@ -656,18 +671,15 @@ public boolean exists(final Object id, final Class<?> entityClass) throws DataAc

@Override
public void dropDatabase() throws DataAccessException {
// guard against NPE because another thread could also call dropDatabase()
ArangoDatabase db = database;
if (db == null) {
db = arango.db(databaseName);
}
final ArangoDatabase db = db();
try {
db.drop();
} catch (final ArangoDBException e) {
throw translateExceptionIfPossible(e);
}
database = null;
collectionCache.clear();
databaseCache.remove(db.name());
collectionCache.keySet().stream().filter(key -> key.getDb().equals(db.name()))
.forEach(key -> collectionCache.remove(key));
}

@Override
Expand Down Expand Up @@ -709,4 +721,11 @@ public ArangoConverter getConverter() {
return this.converter;
}

@Override
public void setApplicationContext(final ApplicationContext applicationContext) throws BeansException {
context.setRootObject(applicationContext);
context.setBeanResolver(new BeanFactoryResolver(applicationContext));
context.addPropertyAccessor(new BeanFactoryAccessor());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.arangodb.springframework.core.template;

class CollectionCacheKey {

private final String db;
private final String collection;

public CollectionCacheKey(final String db, final String collection) {
super();
this.db = db;
this.collection = collection;
}

public String getDb() {
return db;
}

public String getCollection() {
return collection;
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((collection == null) ? 0 : collection.hashCode());
result = prime * result + ((db == null) ? 0 : db.hashCode());
return result;
}

@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final CollectionCacheKey other = (CollectionCacheKey) obj;
if (collection == null) {
if (other.collection != null) {
return false;
}
} else if (!collection.equals(other.collection)) {
return false;
}
if (db == null) {
if (other.db != null) {
return false;
}
} else if (!db.equals(other.db)) {
return false;
}
return true;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.arangodb.springframework.core.template;

import java.util.ArrayList;
import java.util.Collection;

import com.arangodb.ArangoCollection;

class CollectionCacheValue {

private final ArangoCollection collection;
private final Collection<Class<?>> entities;

public CollectionCacheValue(final ArangoCollection collection) {
super();
this.collection = collection;
this.entities = new ArrayList<>();
}

public ArangoCollection getCollection() {
return collection;
}

public Collection<Class<?>> getEntities() {
return entities;
}

public void addEntityClass(final Class<?> entityClass) {
entities.add(entityClass);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,12 @@
public class DefaultCollectionOperations implements CollectionOperations {

private final ArangoCollection collection;
private final Map<String, ArangoCollection> collectionCache;
private final Map<CollectionCacheKey, CollectionCacheValue> collectionCache;
private final PersistenceExceptionTranslator exceptionTranslator;

protected DefaultCollectionOperations(final ArangoCollection collection,
final Map<String, ArangoCollection> collectionCache, final PersistenceExceptionTranslator exceptionTranslator) {
final Map<CollectionCacheKey, CollectionCacheValue> collectionCache,
final PersistenceExceptionTranslator exceptionTranslator) {
this.collection = collection;
this.collectionCache = collectionCache;
this.exceptionTranslator = exceptionTranslator;
Expand All @@ -66,7 +67,7 @@ public String name() {

@Override
public void drop() throws DataAccessException {
collectionCache.remove(collection.name());
collectionCache.remove(new CollectionCacheKey(collection.db().name(), collection.name()));
try {
collection.drop();
} catch (final ArangoDBException e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* DISCLAIMER
*
* Copyright 2017 ArangoDB GmbH, Cologne, Germany
*
* 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.
*
* Copyright holder is ArangoDB GmbH, Cologne, Germany
*/

package com.arangodb.springframework;

import java.util.ArrayList;
import java.util.Collection;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;

import com.arangodb.ArangoDB;
import com.arangodb.springframework.config.AbstractArangoConfiguration;
import com.arangodb.springframework.core.mapping.CustomMappingTest;

/**
*
* @author Mark Vollmary
* @author Christian Lechner
*/
@Configuration
@ComponentScan("com.arangodb.springframework.component")
public class ArangoMultiTenancyTestConfiguration extends AbstractArangoConfiguration {

public static final String DB = "spring-test-db";

@Override
public ArangoDB.Builder arango() {
return new ArangoDB.Builder();
}

@Override
public String database() {
return DB + "#{tenantProvider.getId()}";
}

@Override
protected Collection<Converter<?, ?>> customConverters() {
final Collection<Converter<?, ?>> converters = new ArrayList<>();
converters.add(new CustomMappingTest.CustomVPackReadTestConverter());
converters.add(new CustomMappingTest.CustomVPackWriteTestConverter());
converters.add(new CustomMappingTest.CustomDBEntityReadTestConverter());
converters.add(new CustomMappingTest.CustomDBEntityWriteTestConverter());
return converters;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
* @author Mark Vollmary
*
*/
public class MultiTenancyMappingTest extends AbstractArangoTest {
public class MultiTenancyCollectionLevelMappingTest extends AbstractArangoTest {

@Document("#{tenantProvider.getId()}_collection")
static class MultiTenancyTestEntity {
Expand Down
Loading

0 comments on commit 7224786

Please sign in to comment.