Skip to content

Commit a0e42f5

Browse files
christophstroblodrotbohm
authored andcommitted
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. Original pull request: spring-projects#254.
1 parent 7a3aff1 commit a0e42f5

15 files changed

+1128
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
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.util.ArrayList;
23+
import java.util.Collections;
24+
import java.util.HashSet;
25+
import java.util.List;
26+
import java.util.Set;
27+
28+
import org.bson.types.ObjectId;
29+
import org.springframework.dao.DataAccessException;
30+
import org.springframework.data.mongodb.core.script.CallableMongoScript;
31+
import org.springframework.data.mongodb.core.script.ServerSideJavaScript;
32+
import org.springframework.util.Assert;
33+
import org.springframework.util.CollectionUtils;
34+
import org.springframework.util.ObjectUtils;
35+
import org.springframework.util.StringUtils;
36+
37+
import com.mongodb.DB;
38+
import com.mongodb.MongoException;
39+
40+
/**
41+
* Default implementation of {@link ScriptOperations} capable of saving and executing {@link ServerSideJavaScript}.
42+
*
43+
* @author Christoph Strobl
44+
* @since 1.7
45+
*/
46+
public class DefaultScriptOperations implements ScriptOperations {
47+
48+
private static final String SCRIPT_COLLECTION_NAME = "system.js";
49+
private static final String SCRIPT_NAME_PREFIX = "func_";
50+
private final MongoOperations mongoOperations;
51+
52+
/**
53+
* Creates new {@link DefaultScriptOperations} using given {@link MongoOperations}.
54+
*
55+
* @param mongoOperations must not be {@literal null}.
56+
*/
57+
public DefaultScriptOperations(MongoOperations mongoOperations) {
58+
59+
Assert.notNull(mongoOperations, "MongoOperations must not be null!");
60+
61+
this.mongoOperations = mongoOperations;
62+
}
63+
64+
/*
65+
* (non-Javadoc)
66+
* @see org.springframework.data.mongodb.core.ScriptOperations#save(org.springframework.data.mongodb.core.script.MongoScript)
67+
*/
68+
@Override
69+
public CallableMongoScript register(ServerSideJavaScript script) {
70+
71+
Assert.notNull(script, "Script must not be null!");
72+
73+
CallableMongoScript callableScript = (script instanceof CallableMongoScript) ? (CallableMongoScript) script
74+
: new CallableMongoScript(generateScriptName(), script);
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 ServerSideJavaScript script, final Object... args) {
85+
86+
Assert.notNull(script, "Script must not be null!");
87+
88+
if (script instanceof CallableMongoScript) {
89+
return call(((CallableMongoScript) script).getName(), args);
90+
}
91+
92+
return mongoOperations.execute(new DbCallback<Object>() {
93+
94+
@Override
95+
public Object doInDB(DB db) throws MongoException, DataAccessException {
96+
97+
Assert.notNull(script.getCode(), "Script.code must not be null!");
98+
99+
return db.eval(script.getCode(), convertScriptArgs(args));
100+
}
101+
});
102+
}
103+
104+
/*
105+
* (non-Javadoc)
106+
* @see org.springframework.data.mongodb.core.ScriptOperations#call(java.lang.String, java.lang.Object[])
107+
*/
108+
@Override
109+
public Object call(final String scriptName, final Object... args) {
110+
111+
Assert.hasText(scriptName, "ScriptName must not be null or empty!");
112+
113+
return mongoOperations.execute(new DbCallback<Object>() {
114+
115+
@Override
116+
public Object doInDB(DB db) throws MongoException, DataAccessException {
117+
118+
String evalString = scriptName + "(" + convertAndJoinScriptArgs(args) + ")";
119+
return db.eval(evalString);
120+
}
121+
});
122+
}
123+
124+
/*
125+
* (non-Javadoc)
126+
* @see org.springframework.data.mongodb.core.ScriptOperations#exists(java.lang.String)
127+
*/
128+
@Override
129+
public Boolean exists(String scriptName) {
130+
131+
Assert.hasText(scriptName, "ScriptName must not be null or empty!");
132+
133+
return mongoOperations.exists(query(where("name").is(scriptName)), CallableMongoScript.class,
134+
SCRIPT_COLLECTION_NAME);
135+
}
136+
137+
/*
138+
* (non-Javadoc)
139+
* @see org.springframework.data.mongodb.core.ScriptOperations#scriptNames()
140+
*/
141+
@Override
142+
public Set<String> scriptNames() {
143+
144+
List<CallableMongoScript> scripts = (mongoOperations.findAll(CallableMongoScript.class, SCRIPT_COLLECTION_NAME));
145+
146+
if (CollectionUtils.isEmpty(scripts)) {
147+
return Collections.emptySet();
148+
}
149+
150+
Set<String> scriptNames = new HashSet<String>();
151+
for (CallableMongoScript script : scripts) {
152+
scriptNames.add(script.getName());
153+
}
154+
return scriptNames;
155+
}
156+
157+
/**
158+
* Generate a valid name for the {@literal JavaScript}. MongoDB requires an id of type String for scripts. Calling
159+
* scripts having {@link ObjectId} as id fails. Therefore we create a random UUID without {@code -} (as this won't
160+
* work) an prefix the result with {@link #SCRIPT_NAME_PREFIX}.
161+
*
162+
* @return
163+
*/
164+
private String generateScriptName() {
165+
return SCRIPT_NAME_PREFIX + randomUUID().toString().replaceAll("-", "");
166+
}
167+
168+
private Object[] convertScriptArgs(Object... args) {
169+
170+
if (ObjectUtils.isEmpty(args)) {
171+
return args;
172+
}
173+
174+
List<Object> convertedValues = new ArrayList<Object>(args.length);
175+
for (Object arg : args) {
176+
if (arg instanceof String) {
177+
convertedValues.add("'" + arg + "'");
178+
} else {
179+
convertedValues.add(this.mongoOperations.getConverter().convertToMongoType(arg));
180+
}
181+
}
182+
return convertedValues.toArray();
183+
}
184+
185+
private String convertAndJoinScriptArgs(Object... args) {
186+
187+
if (ObjectUtils.isEmpty(args)) {
188+
return "";
189+
}
190+
191+
return StringUtils.arrayToCommaDelimitedString(convertScriptArgs(args));
192+
}
193+
194+
}

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

+9
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,14 @@ public interface MongoOperations {
284284
*/
285285
IndexOperations indexOps(Class<?> entityClass);
286286

287+
/**
288+
* Returns the {@link ScriptOperations} that can be performed on {@link com.mongodb.DB} level.
289+
*
290+
* @return
291+
* @since 1.7
292+
*/
293+
ScriptOperations scriptOps();
294+
287295
/**
288296
* Query for a list of objects of type T from the collection used by the entity class.
289297
* <p/>
@@ -994,4 +1002,5 @@ <T> T findAndModify(Query query, Update update, FindAndModifyOptions options, Cl
9941002
* @return
9951003
*/
9961004
MongoConverter getConverter();
1005+
9971006
}

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

+9
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,15 @@ public IndexOperations indexOps(Class<?> entityClass) {
540540
return new DefaultIndexOperations(this, determineCollectionName(entityClass));
541541
}
542542

543+
/*
544+
* (non-Javadoc)
545+
* @see org.springframework.data.mongodb.core.MongoOperations#scriptOps()
546+
*/
547+
@Override
548+
public ScriptOperations scriptOps() {
549+
return new DefaultScriptOperations(this);
550+
}
551+
543552
// Find methods that take a Query to express the query and that return a single object.
544553

545554
public <T> T findOne(Query query, Class<T> entityClass) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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.util.Set;
19+
20+
import org.springframework.data.mongodb.core.script.CallableMongoScript;
21+
import org.springframework.data.mongodb.core.script.ServerSideJavaScript;
22+
23+
import com.mongodb.DB;
24+
25+
/**
26+
* Script operations on {@link com.mongodb.DB} level. Allows interaction with server side {@literal JavaScript}
27+
* functions.
28+
*
29+
* @author Christoph Strobl
30+
* @since 1.7
31+
*/
32+
public interface ScriptOperations {
33+
34+
/**
35+
* Store given {@literal script} to {@link com.mongodb.DB} so it can be called via its name.
36+
*
37+
* @param script must not be {@literal null}.
38+
* @return {@link CallableMongoScript} with name under which the {@literal JavaScript} function can be called.
39+
*/
40+
CallableMongoScript register(ServerSideJavaScript script);
41+
42+
/**
43+
* Executes the {@literal script} by either calling it via its {@literal name} or directly sending it.
44+
*
45+
* @param script must not be {@literal null}.
46+
* @param args arguments to pass on for script execution.
47+
* @return the script evaluation result.
48+
* @throws org.springframework.dao.DataAccessException
49+
*/
50+
Object execute(ServerSideJavaScript script, Object... args);
51+
52+
/**
53+
* Call the {@literal JavaScript} by its name.
54+
*
55+
* @param scriptName must not be {@literal null} or empty.
56+
* @param args
57+
* @return
58+
*/
59+
Object call(String scriptName, Object... args);
60+
61+
/**
62+
* Checks {@link DB} for existence of {@link ServerSideJavaScript} with given name.
63+
*
64+
* @param scriptName must not be {@literal null} or empty.
65+
* @return false if no {@link ServerSideJavaScript} with given name exists.
66+
*/
67+
Boolean exists(String scriptName);
68+
69+
/**
70+
* Returns names of {@literal JavaScript} functions that can be called.
71+
*
72+
* @return empty {@link Set} if no scripts found.
73+
*/
74+
Set<String> scriptNames();
75+
76+
}

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

+4
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
import org.springframework.data.mapping.model.SimpleTypeHolder;
4545
import org.springframework.data.mongodb.core.convert.MongoConverters.BigDecimalToStringConverter;
4646
import org.springframework.data.mongodb.core.convert.MongoConverters.BigIntegerToStringConverter;
47+
import org.springframework.data.mongodb.core.convert.MongoConverters.CallableMongoScriptToDBObjectConverter;
48+
import org.springframework.data.mongodb.core.convert.MongoConverters.DBObjectToCallableMongoScriptCoverter;
4749
import org.springframework.data.mongodb.core.convert.MongoConverters.DBObjectToStringConverter;
4850
import org.springframework.data.mongodb.core.convert.MongoConverters.StringToBigDecimalConverter;
4951
import org.springframework.data.mongodb.core.convert.MongoConverters.StringToBigIntegerConverter;
@@ -118,6 +120,8 @@ public CustomConversions(List<?> converters) {
118120
toRegister.add(StringToURLConverter.INSTANCE);
119121
toRegister.add(DBObjectToStringConverter.INSTANCE);
120122
toRegister.add(TermToStringConverter.INSTANCE);
123+
toRegister.add(CallableMongoScriptToDBObjectConverter.INSTANCE);
124+
toRegister.add(DBObjectToCallableMongoScriptCoverter.INSTANCE);
121125

122126
toRegister.addAll(JodaTimeConverters.getConvertersToRegister());
123127
toRegister.addAll(GeoConverters.getConvertersToRegister());

0 commit comments

Comments
 (0)