Skip to content

Commit 9451c0c

Browse files
DATAMONGO-479 - Add support for calling functions.
We added ScriptOperations to MongoTemplate. Those allow storage and execution of java script function directly on the MongoDB server instance. Having ScriptOperations in place builds the foundation for annotation driver support in repository layer.
1 parent a7f2947 commit 9451c0c

14 files changed

+1004
-10
lines changed
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*
2+
* Copyright 2014 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb.core;
17+
18+
import static java.util.UUID.*;
19+
import static org.springframework.data.mongodb.core.query.Criteria.*;
20+
import static org.springframework.data.mongodb.core.query.Query.*;
21+
22+
import java.io.Serializable;
23+
import java.util.ArrayList;
24+
import java.util.List;
25+
26+
import org.springframework.dao.DataAccessException;
27+
import org.springframework.data.mongodb.core.script.CallableMongoScript;
28+
import org.springframework.data.mongodb.core.script.MongoScript;
29+
import org.springframework.util.Assert;
30+
import org.springframework.util.ObjectUtils;
31+
import org.springframework.util.StringUtils;
32+
33+
import com.mongodb.DB;
34+
import com.mongodb.MongoException;
35+
36+
/**
37+
* Default implementation of {@link ScriptOperations} capable of saving and executing simple {@link MongoScript}.
38+
*
39+
* @author Christoph Strobl
40+
* @since 1.7
41+
*/
42+
public class DefaultScriptOperations implements ScriptOperations {
43+
44+
private static final String SCRIPT_COLLECTION_NAME = "system.js";
45+
private static final String SCRIPT_NAME_PREFIX = "func_";
46+
private final MongoOperations mongoOperations;
47+
48+
/**
49+
* Creates new {@link DefaultScriptOperations} using given {@link MongoOperations}.
50+
*
51+
* @param mongoOperations must not be {@literal null}.
52+
*/
53+
public DefaultScriptOperations(MongoOperations mongoOperations) {
54+
55+
Assert.notNull(mongoOperations, "MongoOperations must not be null!");
56+
this.mongoOperations = mongoOperations;
57+
}
58+
59+
/*
60+
* (non-Javadoc)
61+
* @see org.springframework.data.mongodb.core.ScriptOperations#save(org.springframework.data.mongodb.core.script.MongoScript)
62+
*/
63+
@Override
64+
public CallableMongoScript save(MongoScript script) {
65+
66+
Assert.notNull(script, "Script must not be null!");
67+
CallableMongoScript callableScript = null;
68+
69+
if (script instanceof CallableMongoScript) {
70+
callableScript = (CallableMongoScript) script;
71+
} else {
72+
callableScript = new CallableMongoScript(generateScriptName(), script);
73+
}
74+
75+
mongoOperations.save(callableScript, SCRIPT_COLLECTION_NAME);
76+
return callableScript;
77+
}
78+
79+
/*
80+
* (non-Javadoc)
81+
* @see org.springframework.data.mongodb.core.ScriptOperations#execute(org.springframework.data.mongodb.core.script.MongoScript, java.lang.Object[])
82+
*/
83+
@Override
84+
public Object execute(final MongoScript script, final Object... args) {
85+
86+
Assert.notNull(script, "Script must not be null!");
87+
return mongoOperations.execute(new DbCallback<Object>() {
88+
89+
@Override
90+
public Object doInDB(DB db) throws MongoException, DataAccessException {
91+
92+
if (script instanceof CallableMongoScript) {
93+
94+
String evalString = ((CallableMongoScript) script).getName() + "(" + convertAndJoinScriptArgs(args) + ")";
95+
return db.eval(evalString);
96+
}
97+
98+
Assert.notNull(script.getCode(), "Script.code must not be null!");
99+
return db.eval(script.getCode().toString(), convertScriptArgs(args));
100+
}
101+
});
102+
}
103+
104+
/*
105+
* (non-Javadoc)
106+
* @see org.springframework.data.mongodb.core.ScriptOperations#load(java.io.Serializable)
107+
*/
108+
@Override
109+
public CallableMongoScript load(Serializable name) {
110+
111+
Assert.notNull(name, "Name must not be null!");
112+
return mongoOperations.findOne(query(where("name").is(name)), CallableMongoScript.class, SCRIPT_COLLECTION_NAME);
113+
}
114+
115+
/**
116+
* Generate a valid name for the script. MongoDB requires a script id of type String. Calling scripts having ObjectId
117+
* as id fails. Therefore we create a random UUID without {@code -} (as this won't work) an prefix the result with
118+
* {@link #SCRIPT_NAME_PREFIX}.
119+
*
120+
* @return
121+
*/
122+
private String generateScriptName() {
123+
return SCRIPT_NAME_PREFIX + randomUUID().toString().replaceAll("-", "");
124+
}
125+
126+
private Object[] convertScriptArgs(Object... args) {
127+
128+
if (ObjectUtils.isEmpty(args)) {
129+
return args;
130+
}
131+
132+
List<Object> convertedValues = new ArrayList<Object>(args.length);
133+
for (Object arg : args) {
134+
if (arg instanceof String) {
135+
convertedValues.add("'" + arg + "'");
136+
} else {
137+
convertedValues.add(this.mongoOperations.getConverter().convertToMongoType(arg));
138+
}
139+
}
140+
return convertedValues.toArray();
141+
}
142+
143+
private String convertAndJoinScriptArgs(Object... args) {
144+
145+
if (ObjectUtils.isEmpty(args)) {
146+
return "";
147+
}
148+
149+
return StringUtils.arrayToCommaDelimitedString(convertScriptArgs(args));
150+
}
151+
152+
}

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,14 @@ public interface MongoOperations {
249249
*/
250250
IndexOperations indexOps(Class<?> entityClass);
251251

252+
/**
253+
* Returns the {@link ScriptOperations} that can be performed on {@link com.mongodb.DB} level.
254+
*
255+
* @return
256+
* @since 1.7
257+
*/
258+
ScriptOperations scriptOps();
259+
252260
/**
253261
* Query for a list of objects of type T from the collection used by the entity class.
254262
* <p/>
@@ -941,4 +949,5 @@ <T> T findAndModify(Query query, Update update, FindAndModifyOptions options, Cl
941949
* @return
942950
*/
943951
MongoConverter getConverter();
952+
944953
}

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,15 @@ public IndexOperations indexOps(Class<?> entityClass) {
487487
return new DefaultIndexOperations(this, determineCollectionName(entityClass));
488488
}
489489

490+
/*
491+
* (non-Javadoc)
492+
* @see org.springframework.data.mongodb.core.MongoOperations#scriptOps()
493+
*/
494+
@Override
495+
public ScriptOperations scriptOps() {
496+
return new DefaultScriptOperations(this);
497+
}
498+
490499
// Find methods that take a Query to express the query and that return a single object.
491500

492501
public <T> T findOne(Query query, Class<T> entityClass) {
@@ -1012,7 +1021,7 @@ public WriteResult doInCollection(DBCollection collection) throws MongoException
10121021

10131022
if (LOGGER.isDebugEnabled()) {
10141023
LOGGER.debug(String.format("Calling update using query: %s and update: %s in collection: %s",
1015-
serializeToJsonSafely(queryObj), serializeToJsonSafely(updateObj), collectionName));
1024+
serializeToJsonSafely(queryObj), serializeToJsonSafely(updateObj), collectionName));
10161025
}
10171026

10181027
MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.UPDATE, collectionName,
@@ -1189,8 +1198,8 @@ public WriteResult doInCollection(DBCollection collection) throws MongoException
11891198
WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction);
11901199

11911200
if (LOGGER.isDebugEnabled()) {
1192-
LOGGER.debug("Remove using query: {} in collection: {}.",
1193-
new Object[] { serializeToJsonSafely(dboq), collection.getName() });
1201+
LOGGER.debug("Remove using query: {} in collection: {}.", new Object[] { serializeToJsonSafely(dboq),
1202+
collection.getName() });
11941203
}
11951204

11961205
WriteResult wr = writeConcernToUse == null ? collection.remove(dboq) : collection.remove(dboq,
@@ -1664,7 +1673,7 @@ protected <T> T doFindAndRemove(String collectionName, DBObject query, DBObject
16641673
EntityReader<? super T, DBObject> readerToUse = this.mongoConverter;
16651674
if (LOGGER.isDebugEnabled()) {
16661675
LOGGER.debug(String.format("findAndRemove using query: %s fields: %s sort: %s for class: %s in collection: %s",
1667-
serializeToJsonSafely(query), fields, sort, entityClass, collectionName));
1676+
serializeToJsonSafely(query), fields, sort, entityClass, collectionName));
16681677
}
16691678
MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(entityClass);
16701679
return executeFindOneInternal(new FindAndRemoveCallback(queryMapper.getMappedObject(query, entity), fields, sort),
@@ -1688,9 +1697,9 @@ protected <T> T doFindAndModify(String collectionName, DBObject query, DBObject
16881697
DBObject mappedUpdate = updateMapper.getMappedObject(update.getUpdateObject(), entity);
16891698

16901699
if (LOGGER.isDebugEnabled()) {
1691-
LOGGER.debug(String.format("findAndModify using query: %s fields: %s sort: %s for class: %s and update: %s " +
1692-
"in collection: %s", serializeToJsonSafely(mappedQuery), fields, sort, entityClass,
1693-
serializeToJsonSafely(mappedUpdate), collectionName));
1700+
LOGGER.debug(String.format("findAndModify using query: %s fields: %s sort: %s for class: %s and update: %s "
1701+
+ "in collection: %s", serializeToJsonSafely(mappedQuery), fields, sort, entityClass,
1702+
serializeToJsonSafely(mappedUpdate), collectionName));
16941703
}
16951704

16961705
return executeFindOneInternal(new FindAndModifyCallback(mappedQuery, fields, sort, mappedUpdate, options),
@@ -1998,14 +2007,14 @@ public FindOneCallback(DBObject query, DBObject fields) {
19982007
public DBObject doInCollection(DBCollection collection) throws MongoException, DataAccessException {
19992008
if (fields == null) {
20002009
if (LOGGER.isDebugEnabled()) {
2001-
LOGGER.debug(String.format("findOne using query: %s in db.collection: %s",
2002-
serializeToJsonSafely(query), collection.getFullName()));
2010+
LOGGER.debug(String.format("findOne using query: %s in db.collection: %s", serializeToJsonSafely(query),
2011+
collection.getFullName()));
20032012
}
20042013
return collection.findOne(query);
20052014
} else {
20062015
if (LOGGER.isDebugEnabled()) {
20072016
LOGGER.debug(String.format("findOne using query: %s fields: %s in db.collection: %s",
2008-
serializeToJsonSafely(query), fields, collection.getFullName()));
2017+
serializeToJsonSafely(query), fields, collection.getFullName()));
20092018
}
20102019
return collection.findOne(query, fields);
20112020
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright 2014 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb.core;
17+
18+
import java.io.Serializable;
19+
20+
import org.springframework.data.mongodb.core.script.CallableMongoScript;
21+
import org.springframework.data.mongodb.core.script.MongoScript;
22+
23+
/**
24+
* Script operations on {@link com.mongodb.DB} level.
25+
*
26+
* @author Christoph Strobl
27+
* @since 1.7
28+
*/
29+
public interface ScriptOperations {
30+
31+
/**
32+
* Saves given {@literal script} to currently used {@link com.mongodb.DB}.
33+
*
34+
* @param script must not be {@literal null}.
35+
* @return
36+
*/
37+
CallableMongoScript save(MongoScript script);
38+
39+
/**
40+
* Executes the {@literal script} by either calling it via its {@literal _id} or directly sending it.
41+
*
42+
* @param script must not be {@literal null}.
43+
* @param args arguments to pass on for script execution.
44+
* @return the script evaluation result.
45+
* @throws org.springframework.dao.DataAccessException
46+
*/
47+
Object execute(MongoScript script, Object... args);
48+
49+
/**
50+
* Retrieves the {@link CallableMongoScript} by its {@literal id}.
51+
*
52+
* @param name
53+
* @return {@literal null} if not found.
54+
*/
55+
CallableMongoScript load(Serializable name);
56+
57+
}

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/CustomConversions.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242
import org.springframework.data.mapping.model.SimpleTypeHolder;
4343
import org.springframework.data.mongodb.core.convert.MongoConverters.BigDecimalToStringConverter;
4444
import org.springframework.data.mongodb.core.convert.MongoConverters.BigIntegerToStringConverter;
45+
import org.springframework.data.mongodb.core.convert.MongoConverters.CallableMongoScriptToDBObjectConverter;
46+
import org.springframework.data.mongodb.core.convert.MongoConverters.DBObjectToCallableMongoScriptCoverter;
4547
import org.springframework.data.mongodb.core.convert.MongoConverters.DBObjectToStringConverter;
4648
import org.springframework.data.mongodb.core.convert.MongoConverters.StringToBigDecimalConverter;
4749
import org.springframework.data.mongodb.core.convert.MongoConverters.StringToBigIntegerConverter;
@@ -110,6 +112,8 @@ public CustomConversions(List<?> converters) {
110112
toRegister.add(StringToURLConverter.INSTANCE);
111113
toRegister.add(DBObjectToStringConverter.INSTANCE);
112114
toRegister.add(TermToStringConverter.INSTANCE);
115+
toRegister.add(CallableMongoScriptToDBObjectConverter.INSTNACE);
116+
toRegister.add(DBObjectToCallableMongoScriptCoverter.INSTANCE);
113117

114118
toRegister.addAll(JodaTimeConverters.getConvertersToRegister());
115119
toRegister.addAll(GeoConverters.getConvertersToRegister());

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,11 @@
2727
import org.springframework.data.convert.ReadingConverter;
2828
import org.springframework.data.convert.WritingConverter;
2929
import org.springframework.data.mongodb.core.query.Term;
30+
import org.springframework.data.mongodb.core.script.CallableMongoScript;
3031
import org.springframework.util.StringUtils;
3132

33+
import com.mongodb.BasicDBObject;
34+
import com.mongodb.BasicDBObjectBuilder;
3235
import com.mongodb.DBObject;
3336

3437
/**
@@ -178,4 +181,58 @@ public String convert(Term source) {
178181
return source == null ? null : source.getFormatted();
179182
}
180183
}
184+
185+
/**
186+
* @author Christoph Strobl
187+
* @since 1.7
188+
*/
189+
@ReadingConverter
190+
public static enum DBObjectToCallableMongoScriptCoverter implements Converter<DBObject, CallableMongoScript> {
191+
192+
INSTANCE;
193+
194+
@Override
195+
public CallableMongoScript convert(DBObject source) {
196+
197+
if (source == null) {
198+
return null;
199+
}
200+
201+
String id = source.get("_id").toString();
202+
Object rawValue = source.get("value");
203+
204+
if (rawValue != null) {
205+
return new CallableMongoScript(id, rawValue.toString());
206+
}
207+
208+
return new CallableMongoScript(id);
209+
}
210+
}
211+
212+
/**
213+
* @author Christoph Strobl
214+
* @since 1.7
215+
*/
216+
@WritingConverter
217+
public static enum CallableMongoScriptToDBObjectConverter implements Converter<CallableMongoScript, DBObject> {
218+
219+
INSTNACE;
220+
221+
@Override
222+
public DBObject convert(CallableMongoScript source) {
223+
224+
if (source == null) {
225+
return new BasicDBObject();
226+
}
227+
228+
BasicDBObjectBuilder builder = new BasicDBObjectBuilder();
229+
230+
builder.append("_id", source.getName());
231+
if (source.getCode() != null) {
232+
builder.append("value", source.getCode());
233+
}
234+
235+
return builder.get();
236+
}
237+
}
181238
}

0 commit comments

Comments
 (0)