diff --git a/pom.xml b/pom.xml index 93a4e6bf2a..1b2e9ec0f7 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 1.7.0.BUILD-SNAPSHOT + 1.7.0.DATAMONGO-479-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-cross-store/pom.xml b/spring-data-mongodb-cross-store/pom.xml index c7610beee4..e5b1c5698a 100644 --- a/spring-data-mongodb-cross-store/pom.xml +++ b/spring-data-mongodb-cross-store/pom.xml @@ -6,7 +6,7 @@ org.springframework.data spring-data-mongodb-parent - 1.7.0.BUILD-SNAPSHOT + 1.7.0.DATAMONGO-479-SNAPSHOT ../pom.xml @@ -48,7 +48,7 @@ org.springframework.data spring-data-mongodb - 1.7.0.BUILD-SNAPSHOT + 1.7.0.DATAMONGO-479-SNAPSHOT diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index 13110137b6..11cd8642a3 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 1.7.0.BUILD-SNAPSHOT + 1.7.0.DATAMONGO-479-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-log4j/pom.xml b/spring-data-mongodb-log4j/pom.xml index 6ff09e4577..d2fbaba86c 100644 --- a/spring-data-mongodb-log4j/pom.xml +++ b/spring-data-mongodb-log4j/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 1.7.0.BUILD-SNAPSHOT + 1.7.0.DATAMONGO-479-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 7c2d818c42..58ad80e3ba 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -11,7 +11,7 @@ org.springframework.data spring-data-mongodb-parent - 1.7.0.BUILD-SNAPSHOT + 1.7.0.DATAMONGO-479-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultScriptOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultScriptOperations.java new file mode 100644 index 0000000000..44bf3a70c6 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultScriptOperations.java @@ -0,0 +1,194 @@ +/* + * Copyright 2014 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.springframework.data.mongodb.core; + +import static java.util.UUID.*; +import static org.springframework.data.mongodb.core.query.Criteria.*; +import static org.springframework.data.mongodb.core.query.Query.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.bson.types.ObjectId; +import org.springframework.dao.DataAccessException; +import org.springframework.data.mongodb.core.script.CallableMongoScript; +import org.springframework.data.mongodb.core.script.ServerSideJavaScript; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +import com.mongodb.DB; +import com.mongodb.MongoException; + +/** + * Default implementation of {@link ScriptOperations} capable of saving and executing {@link ServerSideJavaScript}. + * + * @author Christoph Strobl + * @since 1.7 + */ +public class DefaultScriptOperations implements ScriptOperations { + + private static final String SCRIPT_COLLECTION_NAME = "system.js"; + private static final String SCRIPT_NAME_PREFIX = "func_"; + private final MongoOperations mongoOperations; + + /** + * Creates new {@link DefaultScriptOperations} using given {@link MongoOperations}. + * + * @param mongoOperations must not be {@literal null}. + */ + public DefaultScriptOperations(MongoOperations mongoOperations) { + + Assert.notNull(mongoOperations, "MongoOperations must not be null!"); + + this.mongoOperations = mongoOperations; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.ScriptOperations#save(org.springframework.data.mongodb.core.script.MongoScript) + */ + @Override + public CallableMongoScript register(ServerSideJavaScript script) { + + Assert.notNull(script, "Script must not be null!"); + + CallableMongoScript callableScript = (script instanceof CallableMongoScript) ? (CallableMongoScript) script + : new CallableMongoScript(generateScriptName(), script); + mongoOperations.save(callableScript, SCRIPT_COLLECTION_NAME); + return callableScript; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.ScriptOperations#execute(org.springframework.data.mongodb.core.script.MongoScript, java.lang.Object[]) + */ + @Override + public Object execute(final ServerSideJavaScript script, final Object... args) { + + Assert.notNull(script, "Script must not be null!"); + + if (script instanceof CallableMongoScript) { + return call(((CallableMongoScript) script).getName(), args); + } + + return mongoOperations.execute(new DbCallback() { + + @Override + public Object doInDB(DB db) throws MongoException, DataAccessException { + + Assert.notNull(script.getCode(), "Script.code must not be null!"); + + return db.eval(script.getCode(), convertScriptArgs(args)); + } + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.ScriptOperations#call(java.lang.String, java.lang.Object[]) + */ + @Override + public Object call(final String scriptName, final Object... args) { + + Assert.hasText(scriptName, "ScriptName must not be null or empty!"); + + return mongoOperations.execute(new DbCallback() { + + @Override + public Object doInDB(DB db) throws MongoException, DataAccessException { + + String evalString = scriptName + "(" + convertAndJoinScriptArgs(args) + ")"; + return db.eval(evalString); + } + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.ScriptOperations#exists(java.lang.String) + */ + @Override + public Boolean exists(String scriptName) { + + Assert.hasText(scriptName, "ScriptName must not be null or empty!"); + + return mongoOperations.exists(query(where("name").is(scriptName)), CallableMongoScript.class, + SCRIPT_COLLECTION_NAME); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.ScriptOperations#scriptNames() + */ + @Override + public Set scriptNames() { + + List scripts = (mongoOperations.findAll(CallableMongoScript.class, SCRIPT_COLLECTION_NAME)); + + if (CollectionUtils.isEmpty(scripts)) { + return Collections.emptySet(); + } + + Set scriptNames = new HashSet(); + for (CallableMongoScript script : scripts) { + scriptNames.add(script.getName()); + } + return scriptNames; + } + + /** + * Generate a valid name for the {@literal JavaScript}. MongoDB requires an id of type String for scripts. Calling + * scripts having {@link ObjectId} as id fails. Therefore we create a random UUID without {@code -} (as this won't + * work) an prefix the result with {@link #SCRIPT_NAME_PREFIX}. + * + * @return + */ + private String generateScriptName() { + return SCRIPT_NAME_PREFIX + randomUUID().toString().replaceAll("-", ""); + } + + private Object[] convertScriptArgs(Object... args) { + + if (ObjectUtils.isEmpty(args)) { + return args; + } + + List convertedValues = new ArrayList(args.length); + for (Object arg : args) { + if (arg instanceof String) { + convertedValues.add("'" + arg + "'"); + } else { + convertedValues.add(this.mongoOperations.getConverter().convertToMongoType(arg)); + } + } + return convertedValues.toArray(); + } + + private String convertAndJoinScriptArgs(Object... args) { + + if (ObjectUtils.isEmpty(args)) { + return ""; + } + + return StringUtils.arrayToCommaDelimitedString(convertScriptArgs(args)); + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java index 6e3261db4f..f35b8698fa 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java @@ -268,6 +268,14 @@ public interface MongoOperations { */ IndexOperations indexOps(Class entityClass); + /** + * Returns the {@link ScriptOperations} that can be performed on {@link com.mongodb.DB} level. + * + * @return + * @since 1.7 + */ + ScriptOperations scriptOps(); + /** * Query for a list of objects of type T from the collection used by the entity class. *

@@ -978,4 +986,5 @@ T findAndModify(Query query, Update update, FindAndModifyOptions options, Cl * @return */ MongoConverter getConverter(); + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index 04c140ace9..1238ff2729 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -513,6 +513,15 @@ public IndexOperations indexOps(Class entityClass) { return new DefaultIndexOperations(this, determineCollectionName(entityClass)); } + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.MongoOperations#scriptOps() + */ + @Override + public ScriptOperations scriptOps() { + return new DefaultScriptOperations(this); + } + // Find methods that take a Query to express the query and that return a single object. public T findOne(Query query, Class entityClass) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ScriptOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ScriptOperations.java new file mode 100644 index 0000000000..c4cd0c5822 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ScriptOperations.java @@ -0,0 +1,76 @@ +/* + * Copyright 2014 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.springframework.data.mongodb.core; + +import java.util.Set; + +import org.springframework.data.mongodb.core.script.CallableMongoScript; +import org.springframework.data.mongodb.core.script.ServerSideJavaScript; + +import com.mongodb.DB; + +/** + * Script operations on {@link com.mongodb.DB} level. Allows interaction with server side {@literal JavaScript} + * functions. + * + * @author Christoph Strobl + * @since 1.7 + */ +public interface ScriptOperations { + + /** + * Store given {@literal script} to {@link com.mongodb.DB} so it can be called via its name. + * + * @param script must not be {@literal null}. + * @return {@link CallableMongoScript} with name under which the {@literal JavaScript} function can be called. + */ + CallableMongoScript register(ServerSideJavaScript script); + + /** + * Executes the {@literal script} by either calling it via its {@literal name} or directly sending it. + * + * @param script must not be {@literal null}. + * @param args arguments to pass on for script execution. + * @return the script evaluation result. + * @throws org.springframework.dao.DataAccessException + */ + Object execute(ServerSideJavaScript script, Object... args); + + /** + * Call the {@literal JavaScript} by its name. + * + * @param scriptName must not be {@literal null} or empty. + * @param args + * @return + */ + Object call(String scriptName, Object... args); + + /** + * Checks {@link DB} for existence of {@link ServerSideJavaScript} with given name. + * + * @param scriptName must not be {@literal null} or empty. + * @return false if no {@link ServerSideJavaScript} with given name exists. + */ + Boolean exists(String scriptName); + + /** + * Returns names of {@literal JavaScript} functions that can be called. + * + * @return empty {@link Set} if no scripts found. + */ + Set scriptNames(); + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/CustomConversions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/CustomConversions.java index cd8c8866db..565bf76d0b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/CustomConversions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/CustomConversions.java @@ -44,6 +44,8 @@ import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.mongodb.core.convert.MongoConverters.BigDecimalToStringConverter; import org.springframework.data.mongodb.core.convert.MongoConverters.BigIntegerToStringConverter; +import org.springframework.data.mongodb.core.convert.MongoConverters.CallableMongoScriptToDBObjectConverter; +import org.springframework.data.mongodb.core.convert.MongoConverters.DBObjectToCallableMongoScriptCoverter; import org.springframework.data.mongodb.core.convert.MongoConverters.DBObjectToStringConverter; import org.springframework.data.mongodb.core.convert.MongoConverters.StringToBigDecimalConverter; import org.springframework.data.mongodb.core.convert.MongoConverters.StringToBigIntegerConverter; @@ -118,6 +120,8 @@ public CustomConversions(List converters) { toRegister.add(StringToURLConverter.INSTANCE); toRegister.add(DBObjectToStringConverter.INSTANCE); toRegister.add(TermToStringConverter.INSTANCE); + toRegister.add(CallableMongoScriptToDBObjectConverter.INSTANCE); + toRegister.add(DBObjectToCallableMongoScriptCoverter.INSTANCE); toRegister.addAll(JodaTimeConverters.getConvertersToRegister()); toRegister.addAll(GeoConverters.getConvertersToRegister()); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java index afabef2558..7c5a19ed98 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java @@ -20,6 +20,7 @@ import java.net.MalformedURLException; import java.net.URL; +import org.bson.types.Code; import org.bson.types.ObjectId; import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.TypeDescriptor; @@ -27,8 +28,11 @@ import org.springframework.data.convert.ReadingConverter; import org.springframework.data.convert.WritingConverter; import org.springframework.data.mongodb.core.query.Term; +import org.springframework.data.mongodb.core.script.CallableMongoScript; import org.springframework.util.StringUtils; +import com.mongodb.BasicDBObject; +import com.mongodb.BasicDBObjectBuilder; import com.mongodb.DBObject; /** @@ -178,4 +182,54 @@ public String convert(Term source) { return source == null ? null : source.getFormatted(); } } + + /** + * @author Christoph Strobl + * @since 1.7 + */ + @ReadingConverter + public static enum DBObjectToCallableMongoScriptCoverter implements Converter { + + INSTANCE; + + @Override + public CallableMongoScript convert(DBObject source) { + + if (source == null) { + return null; + } + + String id = source.get("_id").toString(); + Object rawValue = source.get("value"); + + return new CallableMongoScript(id, ((Code) rawValue).getCode()); + } + } + + /** + * @author Christoph Strobl + * @since 1.7 + */ + @WritingConverter + public static enum CallableMongoScriptToDBObjectConverter implements Converter { + + INSTANCE; + + @Override + public DBObject convert(CallableMongoScript source) { + + if (source == null) { + return new BasicDBObject(); + } + + BasicDBObjectBuilder builder = new BasicDBObjectBuilder(); + + builder.append("_id", source.getName()); + if (source.getCode() != null) { + builder.append("value", new Code(source.getCode())); + } + + return builder.get(); + } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/script/CallableMongoScript.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/script/CallableMongoScript.java new file mode 100644 index 0000000000..b2a7a1e1d5 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/script/CallableMongoScript.java @@ -0,0 +1,80 @@ +/* + * Copyright 2014 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.springframework.data.mongodb.core.script; + +import org.springframework.data.annotation.Id; +import org.springframework.util.Assert; + +/** + * A {@link ServerSideJavaScript} implementation that allows calling the function by its {@literal name} once it has + * been saved to the {@link com.mongodb.DB} instance. + * + * @author Christoph Strobl + * @since 1.7 + */ +public class CallableMongoScript implements ServerSideJavaScript { + + private final @Id String name; + private final ServerSideJavaScript script; + + /** + * Creates new {@link CallableMongoScript} that can be saved to the {@link com.mongodb.DB} instance. + * + * @param name must not be {@literal null} or {@literal empty}. + * @param rawScript the {@link String} representation of the {@literal JavaScript} function. Must not be + * {@literal null} or {@literal empty}. + */ + public CallableMongoScript(String name, String rawScript) { + this(name, new ExecutableMongoScript(rawScript)); + } + + /** + * Creates new {@link CallableMongoScript}. + * + * @param name must not be {@literal null} or {@literal empty}. + * @param script can be {@literal null}. + */ + public CallableMongoScript(String name, ServerSideJavaScript script) { + + Assert.hasText(name, "Name must not be null or empty!"); + this.name = name; + this.script = script; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.script.MongoScript#getCode() + */ + @Override + public String getCode() { + + if (script == null) { + return null; + } + + return script.getCode(); + } + + /** + * Get the name of the {@link CallableMongoScript} script. + * + * @return + */ + public String getName() { + return name; + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/script/ExecutableMongoScript.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/script/ExecutableMongoScript.java new file mode 100644 index 0000000000..8b0082048b --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/script/ExecutableMongoScript.java @@ -0,0 +1,50 @@ +/* + * Copyright 2014 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.springframework.data.mongodb.core.script; + +import org.springframework.util.Assert; + +/** + * {@link ServerSideJavaScript} implementation that can be saved or directly executed. + * + * @author Christoph Strobl + * @since 1.7 + */ +public class ExecutableMongoScript implements ServerSideJavaScript { + + private final String code; + + /** + * Creates new {@link ExecutableMongoScript}. + * + * @param code must not be {@literal null} or {@literal empty}. + */ + public ExecutableMongoScript(String code) { + + Assert.hasText(code, "Code must not be null or empty!"); + this.code = code; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.script.MongoScript#getCode() + */ + @Override + public String getCode() { + return this.code; + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/script/ServerSideJavaScript.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/script/ServerSideJavaScript.java new file mode 100644 index 0000000000..564a8d93cc --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/script/ServerSideJavaScript.java @@ -0,0 +1,30 @@ +/* + * Copyright 2014 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.springframework.data.mongodb.core.script; + +/** + * @author Christoph Strobl + * @since 1.7 + */ +public interface ServerSideJavaScript { + + /** + * Get the {@link String} representation of the JavaScript code. + * + * @return {@literal null} when no code available. + */ + String getCode(); +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultScriptOperationsTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultScriptOperationsTests.java new file mode 100644 index 0000000000..5d49ac3891 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultScriptOperationsTests.java @@ -0,0 +1,204 @@ +/* + * Copyright 2014 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.springframework.data.mongodb.core; + +import static org.hamcrest.collection.IsEmptyCollection.*; +import static org.hamcrest.core.Is.*; +import static org.hamcrest.core.IsCollectionContaining.*; +import static org.junit.Assert.*; +import static org.junit.Assume.*; +import static org.springframework.data.mongodb.core.query.Criteria.*; +import static org.springframework.data.mongodb.core.query.Query.*; + +import org.hamcrest.core.Is; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.UncategorizedDataAccessException; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.script.CallableMongoScript; +import org.springframework.data.mongodb.core.script.ExecutableMongoScript; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import com.mongodb.BasicDBObject; +import com.mongodb.Mongo; +import com.mongodb.MongoClient; + +/** + * @author Christoph Strobl + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class DefaultScriptOperationsTests { + + @Configuration + static class Config { + + private static final String DB_NAME = "script-tests"; + + @Bean + public Mongo mongo() throws Exception { + return new MongoClient(); + } + + @Bean + public MongoTemplate template() throws Exception { + return new MongoTemplate(mongo(), DB_NAME); + } + + } + + static final String JAVASCRIPT_COLLECTION_NAME = "system.js"; + static final String SCRIPT_NAME = "echo"; + static final String JS_FUNCTION = "function(x) { return x; }"; + static final ExecutableMongoScript EXECUTABLE_SCRIPT = new ExecutableMongoScript(JS_FUNCTION); + static final CallableMongoScript CALLABLE_SCRIPT = new CallableMongoScript(SCRIPT_NAME, JS_FUNCTION); + + @Autowired MongoTemplate template; + DefaultScriptOperations scriptOps; + + @Before + public void setUp() { + + template.getCollection(JAVASCRIPT_COLLECTION_NAME).remove(new BasicDBObject()); + this.scriptOps = new DefaultScriptOperations(template); + } + + /** + * @see DATAMONGO-479 + */ + @Test + public void executeShouldDirectlyRunExecutableMongoScript() { + + Object result = scriptOps.execute(EXECUTABLE_SCRIPT, 10); + + assertThat(result, Is. is(10D)); + } + + /** + * @see DATAMONGO-479 + */ + @Test(expected = DataAccessException.class) + public void executeThowsDataAccessExceptionWhenRunningCallableScriptThatHasNotBeenSavedBefore() { + scriptOps.execute(CALLABLE_SCRIPT, 10); + } + + /** + * @see DATAMONGO-479 + */ + @Test + public void saveShouldStoreCallableScriptCorrectly() { + + Query query = query(where("_id").is(SCRIPT_NAME)); + assumeThat(template.exists(query, JAVASCRIPT_COLLECTION_NAME), is(false)); + + scriptOps.register(CALLABLE_SCRIPT); + + assumeThat(template.exists(query, JAVASCRIPT_COLLECTION_NAME), is(true)); + } + + /** + * @see DATAMONGO-479 + */ + @Test + public void saveShouldStoreExecutableScriptCorrectly() { + + CallableMongoScript script = scriptOps.register(EXECUTABLE_SCRIPT); + + Query query = query(where("_id").is(script.getName())); + assumeThat(template.exists(query, JAVASCRIPT_COLLECTION_NAME), is(true)); + } + + /** + * @see DATAMONGO-479 + */ + @Test + public void executeShouldRunCallableScriptThatHasBeenSavedBefore() { + + scriptOps.register(CALLABLE_SCRIPT); + + Query query = query(where("_id").is(SCRIPT_NAME)); + assumeThat(template.exists(query, JAVASCRIPT_COLLECTION_NAME), is(true)); + + Object result = scriptOps.execute(CALLABLE_SCRIPT, 10); + + assertThat(result, Is. is(10D)); + } + + /** + * @see DATAMONGO-479 + */ + @Test + public void existsShouldReturnTrueIfScriptAvailableOnServer() { + + scriptOps.register(CALLABLE_SCRIPT); + + assertThat(scriptOps.exists(SCRIPT_NAME), is(true)); + } + + /** + * @see DATAMONGO-479 + */ + @Test + public void existsShouldReturnFalseIfScriptNotAvailableOnServer() { + assertThat(scriptOps.exists(SCRIPT_NAME), is(false)); + } + + /** + * @see DATAMONGO-479 + */ + @Test + public void callShouldExecuteExistingScript() { + + scriptOps.register(CALLABLE_SCRIPT); + + Object result = scriptOps.call(SCRIPT_NAME, 10); + + assertThat(result, Is. is(10D)); + } + + /** + * @see DATAMONGO-479 + */ + @Test(expected = UncategorizedDataAccessException.class) + public void callShouldThrowExceptionWhenCallingScriptThatDoesNotExist() { + scriptOps.call(SCRIPT_NAME, 10); + } + + /** + * @see DATAMONGO-479 + */ + @Test + public void scriptNamesShouldContainNameOfRegisteredScript() { + + scriptOps.register(CALLABLE_SCRIPT); + + assertThat(scriptOps.scriptNames(), hasItems("echo")); + } + + /** + * @see DATAMONGO-479 + */ + @Test + public void scriptNamesShouldReturnEmptySetWhenNoScriptRegistered() { + assertThat(scriptOps.scriptNames(), empty()); + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultScriptOperationsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultScriptOperationsUnitTests.java new file mode 100644 index 0000000000..89e481eff4 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultScriptOperationsUnitTests.java @@ -0,0 +1,120 @@ +/* + * Copyright 2014 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.springframework.data.mongodb.core; + +import static org.hamcrest.core.IsNull.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.mongodb.core.script.CallableMongoScript; +import org.springframework.data.mongodb.core.script.ExecutableMongoScript; + +/** + * @author Christoph Strobl + * @since 1.7 + */ +@RunWith(MockitoJUnitRunner.class) +public class DefaultScriptOperationsUnitTests { + + DefaultScriptOperations scriptOps; + @Mock MongoOperations mongoOperationsMock; + + @Before + public void setUp() { + this.scriptOps = new DefaultScriptOperations(mongoOperationsMock); + } + + /** + * @see DATAMONGO-479 + */ + @Test(expected = IllegalArgumentException.class) + public void saveShouldThrowExceptionWhenCalledWithNullValue() { + scriptOps.register(null); + } + + /** + * @see DATAMONGO-479 + */ + @Test + public void saveShouldUseCorrectCollectionName() { + + scriptOps.register(new CallableMongoScript("foo", "function...")); + + verify(mongoOperationsMock, times(1)).save(any(CallableMongoScript.class), eq("system.js")); + } + + /** + * @see DATAMONGO-479 + */ + @Test + public void saveShouldGenerateScriptNameForExecutableMongoScripts() { + + scriptOps.register(new ExecutableMongoScript("function...")); + + ArgumentCaptor captor = ArgumentCaptor.forClass(CallableMongoScript.class); + + verify(mongoOperationsMock, times(1)).save(captor.capture(), eq("system.js")); + Assert.assertThat(captor.getValue().getName(), notNullValue()); + } + + /** + * @see DATAMONGO-479 + */ + @Test(expected = IllegalArgumentException.class) + public void executeShouldThrowExceptionWhenScriptIsNull() { + scriptOps.execute(null); + } + + /** + * @see DATAMONGO-479 + */ + @Test(expected = IllegalArgumentException.class) + public void existsShouldThrowExceptionWhenScriptNameIsNull() { + scriptOps.exists(null); + } + + /** + * @see DATAMONGO-479 + */ + @Test(expected = IllegalArgumentException.class) + public void existsShouldThrowExceptionWhenScriptNameIsEmpty() { + scriptOps.exists(""); + } + + /** + * @see DATAMONGO-479 + */ + @Test(expected = IllegalArgumentException.class) + public void callShouldThrowExceptionWhenScriptNameIsNull() { + scriptOps.call(null); + } + + /** + * @see DATAMONGO-479 + */ + @Test(expected = IllegalArgumentException.class) + public void callShouldThrowExceptionWhenScriptNameIsEmpty() { + scriptOps.call(""); + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/CallableMongoScriptConvertsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/CallableMongoScriptConvertsUnitTests.java new file mode 100644 index 0000000000..7d2fc80df7 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/CallableMongoScriptConvertsUnitTests.java @@ -0,0 +1,133 @@ +/* + * Copyright 2014 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.springframework.data.mongodb.core.convert; + +import static org.hamcrest.core.IsEqual.*; +import static org.hamcrest.core.IsInstanceOf.*; +import static org.hamcrest.core.IsNull.*; +import static org.junit.Assert.*; + +import org.bson.types.Code; +import org.hamcrest.core.IsEqual; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Suite; +import org.junit.runners.Suite.SuiteClasses; +import org.springframework.data.mongodb.core.convert.CallableMongoScriptConvertsUnitTests.CallableMongoScriptToDboConverterUnitTests; +import org.springframework.data.mongodb.core.convert.CallableMongoScriptConvertsUnitTests.DboToCallableMongoScriptConverterUnitTests; +import org.springframework.data.mongodb.core.convert.MongoConverters.CallableMongoScriptToDBObjectConverter; +import org.springframework.data.mongodb.core.convert.MongoConverters.DBObjectToCallableMongoScriptCoverter; +import org.springframework.data.mongodb.core.script.CallableMongoScript; + +import com.mongodb.BasicDBObject; +import com.mongodb.BasicDBObjectBuilder; +import com.mongodb.DBObject; + +/** + * @author Christoph Strobl + */ +@RunWith(Suite.class) +@SuiteClasses({ CallableMongoScriptToDboConverterUnitTests.class, DboToCallableMongoScriptConverterUnitTests.class }) +public class CallableMongoScriptConvertsUnitTests { + + static final String FUNCTION_NAME = "echo"; + static final String JS_FUNCTION = "function(x) { return x; }"; + static final CallableMongoScript ECHO_SCRIPT = new CallableMongoScript(FUNCTION_NAME, JS_FUNCTION); + static final DBObject FUNCTION = new BasicDBObjectBuilder().add("_id", FUNCTION_NAME) + .add("value", new Code(JS_FUNCTION)).get(); + + /** + * @author Christoph Strobl + */ + public static class CallableMongoScriptToDboConverterUnitTests { + + CallableMongoScriptToDBObjectConverter converter = CallableMongoScriptToDBObjectConverter.INSTANCE; + + /** + * @see DATAMONGO-479 + */ + @Test + public void convertShouldReturnEmptyDboWhenScriptIsNull() { + assertThat(converter.convert(null), IsEqual. equalTo(new BasicDBObject())); + } + + /** + * @see DATAMONGO-479 + */ + @Test + public void convertShouldConvertScriptNameCorreclty() { + + DBObject dbo = converter.convert(ECHO_SCRIPT); + + Object id = dbo.get("_id"); + assertThat(id, instanceOf(String.class)); + assertThat(id, IsEqual. equalTo(FUNCTION_NAME)); + } + + /** + * @see DATAMONGO-479 + */ + @Test + public void convertShouldConvertScriptCodeCorreclty() { + + DBObject dbo = converter.convert(ECHO_SCRIPT); + + Object code = dbo.get("value"); + assertThat(code, instanceOf(Code.class)); + assertThat(code, equalTo((Object) new Code(JS_FUNCTION))); + } + } + + /** + * @author Christoph Strobl + */ + public static class DboToCallableMongoScriptConverterUnitTests { + + DBObjectToCallableMongoScriptCoverter converter = DBObjectToCallableMongoScriptCoverter.INSTANCE; + + /** + * @see DATAMONGO-479 + */ + @Test + public void convertShouldReturnNullIfSourceIsNull() { + assertThat(converter.convert(null), nullValue()); + } + + /** + * @see DATAMONGO-479 + */ + @Test + public void convertShouldConvertIdCorreclty() { + + CallableMongoScript script = converter.convert(FUNCTION); + + assertThat(script.getName(), equalTo(FUNCTION_NAME)); + } + + /** + * @see DATAMONGO-479 + */ + @Test + public void convertShouldConvertScriptValueCorreclty() { + + CallableMongoScript script = converter.convert(FUNCTION); + + assertThat(script.getCode(), notNullValue()); + assertThat(script.getCode(), equalTo(JS_FUNCTION)); + } + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/script/CallableMongoScriptUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/script/CallableMongoScriptUnitTests.java new file mode 100644 index 0000000000..aa45510b2a --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/script/CallableMongoScriptUnitTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2014 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.springframework.data.mongodb.core.script; + +import static org.hamcrest.core.IsEqual.*; +import static org.junit.Assert.*; + +import org.junit.Test; + +/** + * @author Christoph Strobl + */ +public class CallableMongoScriptUnitTests { + + /** + * @see DATAMONGO-479 + */ + @Test(expected = IllegalArgumentException.class) + public void shouldThrowExceptionWhenScriptNameIsNull() { + new CallableMongoScript(null, "return 1;"); + } + + /** + * @see DATAMONGO-479 + */ + @Test(expected = IllegalArgumentException.class) + public void shouldThrowExceptionWhenScriptNameIsEmptyString() { + new CallableMongoScript("", "return 1"); + } + + /** + * @see DATAMONGO-479 + */ + @Test(expected = IllegalArgumentException.class) + public void shouldThrowExceptionWhenRawScriptIsEmptyString() { + new CallableMongoScript("foo", ""); + } + + /** + * @see DATAMONGO-479 + */ + @Test + public void getCodeShouldReturnCodeRepresentationOfRawScript() { + + String jsFunction = "function(x) { return x; }"; + + CallableMongoScript script = new CallableMongoScript("echo", jsFunction); + + assertThat(script.getCode(), equalTo(jsFunction)); + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/script/ExecutableMongoScriptUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/script/ExecutableMongoScriptUnitTests.java new file mode 100644 index 0000000000..a8fda1fad1 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/script/ExecutableMongoScriptUnitTests.java @@ -0,0 +1,81 @@ +/* + * Copyright 2014 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.springframework.data.mongodb.core.script; + +import static org.hamcrest.core.IsEqual.*; +import static org.hamcrest.core.IsNull.*; +import static org.junit.Assert.*; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.util.ObjectUtils; + +/** + * @author Christoph Strobl + */ +public class ExecutableMongoScriptUnitTests { + + public @Rule ExpectedException expectedException = ExpectedException.none(); + + /** + * @see DATAMONGO-479 + */ + @Test + public void constructorShouldThrowExceptionWhenRawScriptIsNull() { + + expectException(IllegalArgumentException.class, "must not be", "null"); + + new ExecutableMongoScript(null); + } + + /** + * @see DATAMONGO-479 + */ + @Test + public void constructorShouldThrowExceptionWhenRawScriptIsEmpty() { + + expectException(IllegalArgumentException.class, "must not be", "empty"); + + new ExecutableMongoScript(""); + } + + /** + * @see DATAMONGO-479 + */ + @Test + public void getCodeShouldReturnCodeRepresentationOfRawScript() { + + String jsFunction = "function(x) { return x; }"; + + ExecutableMongoScript script = new ExecutableMongoScript(jsFunction); + + assertThat(script.getCode(), notNullValue()); + assertThat(script.getCode().toString(), equalTo(jsFunction)); + } + + private void expectException(Class type, String... messageFragments) { + + expectedException.expect(IllegalArgumentException.class); + + if (!ObjectUtils.isEmpty(messageFragments)) { + for (String fragment : messageFragments) { + expectedException.expectMessage(fragment); + } + } + } + +} diff --git a/src/main/asciidoc/reference/mongodb.adoc b/src/main/asciidoc/reference/mongodb.adoc index 6697716dbb..4376fd0d89 100644 --- a/src/main/asciidoc/reference/mongodb.adoc +++ b/src/main/asciidoc/reference/mongodb.adoc @@ -1325,6 +1325,25 @@ MapReduceResults results = mongoOperations.mapReduce(query, "jmr1", Note that you can specify additional limit and sort values as well on the query but not skip values. +[[mongo.server-side-scripts]] +== Script Operations + +MongoDB allows to execute JavaScript functions on the server by either directly sending the script or calling a stored one. `ScriptOperations` can be accessed via `MongoTemplate` and provides basic abstraction for `JavaScript` usage. + +=== Example Usage + +[source,java] +---- +ScriptOperations scriptOps = template.scriptOps(); + +ServerSideJavaScript echoScript = new ExecutableMongoScript("function(x) { return x; }"); +scriptOps.execute(echoScript, "directly execute script"); + +scriptOps.register(new CallableMongoScript("echo", echoScript)); +scriptOps.call("echo", "execute script via name"); +---- + + [[mongo.group]] == Group Operations