Skip to content

Commit 7224786

Browse files
authored
Add SpEL expression parsing for database names (#103)
1 parent 176f5da commit 7224786

File tree

8 files changed

+313
-38
lines changed

8 files changed

+313
-38
lines changed

ChangeLog.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
1818
- added convenience method `ArangoOperations#query(String, Map<String, Object>, Class)`
1919
- added support for non-String `@Id`s (issue #79)
2020
- added convenience method `AbstractArangoConfiguration#customConverters()` to add custom converters
21+
- added SpEL expression parsing for database names
22+
23+
SpEL expressions can now be used within `AbstractArangoConfiguration#database()`. This allows Multi-tenancy on database level.
2124

2225
### Changed
2326

src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java

Lines changed: 53 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,19 @@
3333
import java.util.stream.Collectors;
3434
import java.util.stream.StreamSupport;
3535

36+
import org.springframework.beans.BeansException;
37+
import org.springframework.context.ApplicationContext;
38+
import org.springframework.context.ApplicationContextAware;
39+
import org.springframework.context.expression.BeanFactoryAccessor;
40+
import org.springframework.context.expression.BeanFactoryResolver;
3641
import org.springframework.dao.DataAccessException;
3742
import org.springframework.dao.support.PersistenceExceptionTranslator;
3843
import org.springframework.data.domain.Persistable;
3944
import org.springframework.data.mapping.PersistentPropertyAccessor;
45+
import org.springframework.expression.Expression;
46+
import org.springframework.expression.ParserContext;
47+
import org.springframework.expression.spel.standard.SpelExpressionParser;
48+
import org.springframework.expression.spel.support.StandardEvaluationContext;
4049

4150
import com.arangodb.ArangoCollection;
4251
import com.arangodb.ArangoCursor;
@@ -81,15 +90,20 @@
8190
* @author Christian Lechner
8291
* @author Reşat SABIQ
8392
*/
84-
public class ArangoTemplate implements ArangoOperations, CollectionCallback {
93+
public class ArangoTemplate implements ArangoOperations, CollectionCallback, ApplicationContextAware {
94+
95+
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
8596

8697
private volatile ArangoDBVersion version;
8798
private final PersistenceExceptionTranslator exceptionTranslator;
8899
private final ArangoConverter converter;
89100
private final ArangoDB arango;
90-
private volatile ArangoDatabase database;
91101
private final String databaseName;
92-
private final Map<String, ArangoCollection> collectionCache;
102+
private final Expression databaseExpression;
103+
private final Map<String, ArangoDatabase> databaseCache;
104+
private final Map<CollectionCacheKey, CollectionCacheValue> collectionCache;
105+
106+
private final StandardEvaluationContext context;
93107

94108
public ArangoTemplate(final ArangoDB arango, final String database) {
95109
this(arango, database, null);
@@ -105,32 +119,26 @@ public ArangoTemplate(final ArangoDB arango, final String database, final Arango
105119
this.arango = arango._setCursorInitializer(
106120
new com.arangodb.springframework.core.template.ArangoCursorInitializer(converter));
107121
this.databaseName = database;
122+
this.databaseExpression = PARSER.parseExpression(databaseName, ParserContext.TEMPLATE_EXPRESSION);
108123
this.converter = converter;
109124
this.exceptionTranslator = exceptionTranslator;
125+
this.context = new StandardEvaluationContext();
110126
// set concurrency level to 1 as writes are very rare compared to reads
111127
collectionCache = new ConcurrentHashMap<>(8, 0.9f, 1);
128+
databaseCache = new ConcurrentHashMap<>(8, 0.9f, 1);
112129
version = null;
113130
}
114131

115132
private ArangoDatabase db() {
116-
// guard against NPE because database can be set to null by dropDatabase() by another thread
117-
ArangoDatabase db = database;
118-
if (db != null) {
119-
return db;
120-
}
121-
// make sure the database is only created once
122-
synchronized (this) {
123-
db = database;
124-
if (db != null) {
125-
return db;
126-
}
127-
db = arango.db(databaseName);
133+
final String key = databaseExpression != null ? databaseExpression.getValue(context, String.class)
134+
: databaseName;
135+
return databaseCache.computeIfAbsent(key, name -> {
136+
final ArangoDatabase db = arango.db(name);
128137
if (!db.exists()) {
129138
db.create();
130139
}
131-
database = db;
132140
return db;
133-
}
141+
});
134142
}
135143

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

160-
return collectionCache.computeIfAbsent(name, collName -> {
161-
final ArangoCollection collection = db().collection(collName);
162-
if (!collection.exists()) {
163-
collection.create(options);
164-
}
165-
if (persistentEntity != null) {
166-
ensureCollectionIndexes(collection(collection), persistentEntity);
167-
}
168-
return collection;
169-
});
168+
final ArangoDatabase db = db();
169+
final Class<?> entityClass = persistentEntity != null ? persistentEntity.getType() : null;
170+
final CollectionCacheValue value = collectionCache.computeIfAbsent(new CollectionCacheKey(db.name(), name),
171+
key -> {
172+
final ArangoCollection collection = db.collection(name);
173+
if (!collection.exists()) {
174+
collection.create(options);
175+
}
176+
return new CollectionCacheValue(collection);
177+
});
178+
final Collection<Class<?>> entities = value.getEntities();
179+
final ArangoCollection collection = value.getCollection();
180+
if (persistentEntity != null && !entities.contains(entityClass)) {
181+
value.addEntityClass(entityClass);
182+
ensureCollectionIndexes(collection(collection), persistentEntity);
183+
}
184+
return collection;
170185
}
171186

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

657672
@Override
658673
public void dropDatabase() throws DataAccessException {
659-
// guard against NPE because another thread could also call dropDatabase()
660-
ArangoDatabase db = database;
661-
if (db == null) {
662-
db = arango.db(databaseName);
663-
}
674+
final ArangoDatabase db = db();
664675
try {
665676
db.drop();
666677
} catch (final ArangoDBException e) {
667678
throw translateExceptionIfPossible(e);
668679
}
669-
database = null;
670-
collectionCache.clear();
680+
databaseCache.remove(db.name());
681+
collectionCache.keySet().stream().filter(key -> key.getDb().equals(db.name()))
682+
.forEach(key -> collectionCache.remove(key));
671683
}
672684

673685
@Override
@@ -709,4 +721,11 @@ public ArangoConverter getConverter() {
709721
return this.converter;
710722
}
711723

724+
@Override
725+
public void setApplicationContext(final ApplicationContext applicationContext) throws BeansException {
726+
context.setRootObject(applicationContext);
727+
context.setBeanResolver(new BeanFactoryResolver(applicationContext));
728+
context.addPropertyAccessor(new BeanFactoryAccessor());
729+
}
730+
712731
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package com.arangodb.springframework.core.template;
2+
3+
class CollectionCacheKey {
4+
5+
private final String db;
6+
private final String collection;
7+
8+
public CollectionCacheKey(final String db, final String collection) {
9+
super();
10+
this.db = db;
11+
this.collection = collection;
12+
}
13+
14+
public String getDb() {
15+
return db;
16+
}
17+
18+
public String getCollection() {
19+
return collection;
20+
}
21+
22+
@Override
23+
public int hashCode() {
24+
final int prime = 31;
25+
int result = 1;
26+
result = prime * result + ((collection == null) ? 0 : collection.hashCode());
27+
result = prime * result + ((db == null) ? 0 : db.hashCode());
28+
return result;
29+
}
30+
31+
@Override
32+
public boolean equals(final Object obj) {
33+
if (this == obj) {
34+
return true;
35+
}
36+
if (obj == null) {
37+
return false;
38+
}
39+
if (getClass() != obj.getClass()) {
40+
return false;
41+
}
42+
final CollectionCacheKey other = (CollectionCacheKey) obj;
43+
if (collection == null) {
44+
if (other.collection != null) {
45+
return false;
46+
}
47+
} else if (!collection.equals(other.collection)) {
48+
return false;
49+
}
50+
if (db == null) {
51+
if (other.db != null) {
52+
return false;
53+
}
54+
} else if (!db.equals(other.db)) {
55+
return false;
56+
}
57+
return true;
58+
}
59+
60+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.arangodb.springframework.core.template;
2+
3+
import java.util.ArrayList;
4+
import java.util.Collection;
5+
6+
import com.arangodb.ArangoCollection;
7+
8+
class CollectionCacheValue {
9+
10+
private final ArangoCollection collection;
11+
private final Collection<Class<?>> entities;
12+
13+
public CollectionCacheValue(final ArangoCollection collection) {
14+
super();
15+
this.collection = collection;
16+
this.entities = new ArrayList<>();
17+
}
18+
19+
public ArangoCollection getCollection() {
20+
return collection;
21+
}
22+
23+
public Collection<Class<?>> getEntities() {
24+
return entities;
25+
}
26+
27+
public void addEntityClass(final Class<?> entityClass) {
28+
entities.add(entityClass);
29+
}
30+
31+
}

src/main/java/com/arangodb/springframework/core/template/DefaultCollectionOperations.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,12 @@
4545
public class DefaultCollectionOperations implements CollectionOperations {
4646

4747
private final ArangoCollection collection;
48-
private final Map<String, ArangoCollection> collectionCache;
48+
private final Map<CollectionCacheKey, CollectionCacheValue> collectionCache;
4949
private final PersistenceExceptionTranslator exceptionTranslator;
5050

5151
protected DefaultCollectionOperations(final ArangoCollection collection,
52-
final Map<String, ArangoCollection> collectionCache, final PersistenceExceptionTranslator exceptionTranslator) {
52+
final Map<CollectionCacheKey, CollectionCacheValue> collectionCache,
53+
final PersistenceExceptionTranslator exceptionTranslator) {
5354
this.collection = collection;
5455
this.collectionCache = collectionCache;
5556
this.exceptionTranslator = exceptionTranslator;
@@ -66,7 +67,7 @@ public String name() {
6667

6768
@Override
6869
public void drop() throws DataAccessException {
69-
collectionCache.remove(collection.name());
70+
collectionCache.remove(new CollectionCacheKey(collection.db().name(), collection.name()));
7071
try {
7172
collection.drop();
7273
} catch (final ArangoDBException e) {
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* DISCLAIMER
3+
*
4+
* Copyright 2017 ArangoDB GmbH, Cologne, Germany
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*
18+
* Copyright holder is ArangoDB GmbH, Cologne, Germany
19+
*/
20+
21+
package com.arangodb.springframework;
22+
23+
import java.util.ArrayList;
24+
import java.util.Collection;
25+
26+
import org.springframework.context.annotation.ComponentScan;
27+
import org.springframework.context.annotation.Configuration;
28+
import org.springframework.core.convert.converter.Converter;
29+
30+
import com.arangodb.ArangoDB;
31+
import com.arangodb.springframework.config.AbstractArangoConfiguration;
32+
import com.arangodb.springframework.core.mapping.CustomMappingTest;
33+
34+
/**
35+
*
36+
* @author Mark Vollmary
37+
* @author Christian Lechner
38+
*/
39+
@Configuration
40+
@ComponentScan("com.arangodb.springframework.component")
41+
public class ArangoMultiTenancyTestConfiguration extends AbstractArangoConfiguration {
42+
43+
public static final String DB = "spring-test-db";
44+
45+
@Override
46+
public ArangoDB.Builder arango() {
47+
return new ArangoDB.Builder();
48+
}
49+
50+
@Override
51+
public String database() {
52+
return DB + "#{tenantProvider.getId()}";
53+
}
54+
55+
@Override
56+
protected Collection<Converter<?, ?>> customConverters() {
57+
final Collection<Converter<?, ?>> converters = new ArrayList<>();
58+
converters.add(new CustomMappingTest.CustomVPackReadTestConverter());
59+
converters.add(new CustomMappingTest.CustomVPackWriteTestConverter());
60+
converters.add(new CustomMappingTest.CustomDBEntityReadTestConverter());
61+
converters.add(new CustomMappingTest.CustomDBEntityWriteTestConverter());
62+
return converters;
63+
}
64+
65+
}

src/test/java/com/arangodb/springframework/core/mapping/MultiTenancyMappingTest.java renamed to src/test/java/com/arangodb/springframework/core/mapping/MultiTenancyCollectionLevelMappingTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
* @author Mark Vollmary
3636
*
3737
*/
38-
public class MultiTenancyMappingTest extends AbstractArangoTest {
38+
public class MultiTenancyCollectionLevelMappingTest extends AbstractArangoTest {
3939

4040
@Document("#{tenantProvider.getId()}_collection")
4141
static class MultiTenancyTestEntity {

0 commit comments

Comments
 (0)