diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/AntPath.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/AntPath.java new file mode 100644 index 0000000000..de20bc35db --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/AntPath.java @@ -0,0 +1,106 @@ +/* + * Copyright 2011 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.gridfs; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.util.Assert; + +/** + * Value object to abstract Ant paths. + * + * @author Oliver Gierke + */ +class AntPath { + + private static final String PREFIX_DELIMITER = ":"; + private static final Pattern WILDCARD_PATTERN = Pattern.compile("\\?|\\*\\*|\\*"); + + private final String path; + + /** + * Creates a new {@link AntPath} from the given path. + * + * @param path must not be {@literal null}. + */ + public AntPath(String path) { + Assert.notNull(path); + this.path = path; + } + + /** + * Returns whether the path is a pattern. + * + * @return + */ + public boolean isPattern() { + String path = stripPrefix(this.path); + return (path.indexOf('*') != -1 || path.indexOf('?') != -1); + } + + private static String stripPrefix(String path) { + int index = path.indexOf(PREFIX_DELIMITER); + return (index > -1 ? path.substring(index + 1) : path); + } + + /** + * Returns the regular expression equivalent of this Ant path. + * + * @return + */ + public String toRegex() { + + StringBuilder patternBuilder = new StringBuilder(); + Matcher m = WILDCARD_PATTERN.matcher(path); + int end = 0; + + while (m.find()) { + + patternBuilder.append(quote(path, end, m.start())); + String match = m.group(); + + if ("?".equals(match)) { + patternBuilder.append('.'); + } else if ("**".equals(match)) { + patternBuilder.append(".*"); + } else if ("*".equals(match)) { + patternBuilder.append("[^/]*"); + } + + end = m.end(); + } + + patternBuilder.append(quote(path, end, path.length())); + return patternBuilder.toString(); + } + + private static String quote(String s, int start, int end) { + if (start == end) { + return ""; + } + return Pattern.quote(s.substring(start, end)); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return path; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsCriteria.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsCriteria.java new file mode 100644 index 0000000000..6a92fabf7b --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsCriteria.java @@ -0,0 +1,73 @@ +/* + * Copyright 2011 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.gridfs; + +import org.springframework.data.mongodb.core.query.Criteria; + +/** + * GridFs-specific helper class to define {@link Criteria}s. + * + * @author Oliver Gierke + */ +public class GridFsCriteria extends Criteria { + + /** + * Creates a new {@link GridFsCriteria} for the given key. + * + * @param key + */ + public GridFsCriteria(String key) { + super(key); + } + + /** + * Creates a {@link GridFsCriteria} for restrictions on the file's metadata. + * + * @return + */ + public static GridFsCriteria whereMetaData() { + return new GridFsCriteria("metadata"); + } + + /** + * Creates a {@link GridFsCriteria} for restrictions on a single file's metadata item. + * + * @param metadataKey + * @return + */ + public static GridFsCriteria whereMetaData(String metadataKey) { + String extension = metadataKey == null ? "" : "." + metadataKey; + return new GridFsCriteria(String.format("metadata%s", extension)); + } + + /** + * Creates a {@link GridFsCriteria} for restrictions on the file's name. + * + * @return + */ + public static GridFsCriteria whereFilename() { + return new GridFsCriteria("filename"); + } + + /** + * Creates a {@link GridFsCriteria} for restrictions on the file's content type. + * + * @return + */ + public static GridFsCriteria whereContentType() { + return new GridFsCriteria("contentType"); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsOperations.java new file mode 100644 index 0000000000..e465711cfc --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsOperations.java @@ -0,0 +1,105 @@ +/* + * Copyright 2011 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.gridfs; + +import java.io.InputStream; +import java.util.List; + +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.data.mongodb.core.query.Query; + +import com.mongodb.DBObject; +import com.mongodb.gridfs.GridFSDBFile; +import com.mongodb.gridfs.GridFSFile; + +/** + * Collection of operations to store and read files from MongoDB GridFS. + * + * @author Oliver Gierke + */ +public interface GridFsOperations extends ResourcePatternResolver { + + /** + * Stores the given content into a file with the given name. + * + * @param content must not be {@literal null}. + * @param filename must not be {@literal null} or empty. + * @return the {@link GridFSFile} just created + */ + GridFSFile store(InputStream content, String filename); + + /** + * Stores the given content into a file with the given name using the given metadata. The metadata object will be + * marshalled before writing. + * + * @param content must not be {@literal null}. + * @param filename must not be {@literal null} or empty. + * @param metadata + * @return the {@link GridFSFile} just created + */ + GridFSFile store(InputStream content, String filename, Object metadata); + + /** + * Stores the given content into a file with the given name using the given metadata. + * + * @param content must not be {@literal null}. + * @param filename must not be {@literal null} or empty. + * @param metadata must not be {@literal null}. + * @return the {@link GridFSFile} just created + */ + GridFSFile store(InputStream content, String filename, DBObject metadata); + + /** + * Returns all files matching the given query. + * + * @param query + * @return + */ + List find(Query query); + + /** + * Returns a single file matching the given query or {@literal null} in case no file matches. + * + * @param query + * @return + */ + GridFSDBFile findOne(Query query); + + /** + * Deletes all files matching the given {@link Query}. + * + * @param query + */ + void delete(Query query); + + /** + * Returns all {@link GridFsResource} with the given file name. + * + * @param filename + * @return + * @see ResourcePatternResolver#getResource(String) + */ + GridFsResource getResource(String filename); + + /** + * Returns all {@link GridFsResource}s matching the given file name pattern. + * + * @param filenamePattern + * @return + * @see ResourcePatternResolver#getResources(String) + */ + GridFsResource[] getResources(String filenamePattern); +} \ No newline at end of file diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsResource.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsResource.java new file mode 100644 index 0000000000..8a475367d3 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsResource.java @@ -0,0 +1,88 @@ +/* + * Copyright 2011 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.gridfs; + +import java.io.IOException; + +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; + +import com.mongodb.gridfs.GridFSDBFile; + +/** + * {@link GridFSDBFile} based {@link Resource} implementation. + * + * @author Oliver Gierke + */ +public class GridFsResource extends InputStreamResource { + + private final GridFSDBFile file; + + /** + * Creates a new {@link GridFsResource} from the given {@link GridFSDBFile}. + * + * @param file must not be {@literal null}. + */ + public GridFsResource(GridFSDBFile file) { + super(file.getInputStream()); + this.file = file; + } + + /* + * (non-Javadoc) + * @see org.springframework.core.io.AbstractResource#contentLength() + */ + @Override + public long contentLength() throws IOException { + return file.getLength(); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.io.AbstractResource#getFilename() + */ + @Override + public String getFilename() throws IllegalStateException { + return file.getFilename(); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.io.AbstractResource#lastModified() + */ + @Override + public long lastModified() throws IOException { + return file.getUploadDate().getTime(); + } + + /** + * Returns the {@link Resource}'s id. + * + * @return + */ + public Object getId() { + return file.getId(); + } + + /** + * Returns the {@link Resource}'s content type. + * + * @return + */ + public String getContentType() { + return file.getContentType(); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsTemplate.java new file mode 100644 index 0000000000..b0ce74de48 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsTemplate.java @@ -0,0 +1,194 @@ +/* + * Copyright 2011 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.gridfs; + +import static org.springframework.data.mongodb.core.query.Query.*; +import static org.springframework.data.mongodb.gridfs.GridFsCriteria.*; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.data.mongodb.MongoDbFactory; +import org.springframework.data.mongodb.core.QueryMapper; +import org.springframework.data.mongodb.core.convert.MongoConverter; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import com.mongodb.BasicDBObject; +import com.mongodb.DB; +import com.mongodb.DBObject; +import com.mongodb.gridfs.GridFS; +import com.mongodb.gridfs.GridFSDBFile; +import com.mongodb.gridfs.GridFSFile; +import com.mongodb.gridfs.GridFSInputFile; + +/** + * {@link GridFsOperations} implementation to store content into MongoDB GridFS. + * + * @author Oliver Gierke + */ +public class GridFsTemplate implements GridFsOperations, ResourcePatternResolver { + + private final MongoDbFactory dbFactory; + private final String bucket; + private final MongoConverter converter; + private final QueryMapper queryMapper; + + /** + * Creates a new {@link GridFsTemplate} using the given {@link MongoDbFactory} and {@link MongoConverter}. + * + * @param dbFactory must not be {@literal null}. + * @param converter must not be {@literal null}. + */ + public GridFsTemplate(MongoDbFactory dbFactory, MongoConverter converter) { + this(dbFactory, converter, null); + } + + /** + * Creates a new {@link GridFsTemplate} using the given {@link MongoDbFactory} and {@link MongoConverter}. + * + * @param dbFactory must not be {@literal null}. + * @param converter must not be {@literal null}. + * @param bucket + */ + public GridFsTemplate(MongoDbFactory dbFactory, MongoConverter converter, String bucket) { + + Assert.notNull(dbFactory); + Assert.notNull(converter); + + this.dbFactory = dbFactory; + this.converter = converter; + this.bucket = bucket; + + this.queryMapper = new QueryMapper(converter); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.gridfs.GridFsOperations#store(java.io.InputStream, java.lang.String) + */ + public GridFSFile store(InputStream content, String filename) { + return store(content, filename, (Object) null); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.gridfs.GridFsOperations#store(java.io.InputStream, java.lang.String, java.lang.Object) + */ + public GridFSFile store(InputStream content, String filename, Object metadata) { + + DBObject dbObject = new BasicDBObject(); + converter.write(metadata, dbObject); + return store(content, filename, dbObject); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.gridfs.GridFsOperations#store(java.io.InputStream, java.lang.String, com.mongodb.DBObject) + */ + public GridFSFile store(InputStream content, String filename, DBObject metadata) { + + Assert.notNull(content); + Assert.hasText(filename); + Assert.notNull(metadata); + + GridFSInputFile file = getGridFs().createFile(content); + file.setFilename(filename); + file.setMetaData(metadata); + file.save(); + + return file; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.gridfs.GridFsOperations#find(com.mongodb.DBObject) + */ + public List find(Query query) { + return getGridFs().find(getMappedQuery(query)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.gridfs.GridFsOperations#findOne(com.mongodb.DBObject) + */ + public GridFSDBFile findOne(Query query) { + return getGridFs().findOne(getMappedQuery(query)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.gridfs.GridFsOperations#delete(org.springframework.data.mongodb.core.query.Query) + */ + public void delete(Query query) { + getGridFs().remove(getMappedQuery(query)); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.io.ResourceLoader#getClassLoader() + */ + public ClassLoader getClassLoader() { + return dbFactory.getClass().getClassLoader(); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.io.ResourceLoader#getResource(java.lang.String) + */ + public GridFsResource getResource(String location) { + return new GridFsResource(findOne(query(whereFilename().is(location)))); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.io.support.ResourcePatternResolver#getResources(java.lang.String) + */ + public GridFsResource[] getResources(String locationPattern) { + + if (!StringUtils.hasText(locationPattern)) { + return new GridFsResource[0]; + } + + AntPath path = new AntPath(locationPattern); + + if (path.isPattern()) { + + List files = find(query(whereFilename().regex(path.toRegex()))); + List resources = new ArrayList(files.size()); + + for (GridFSDBFile file : files) { + resources.add(new GridFsResource(file)); + } + + return resources.toArray(new GridFsResource[resources.size()]); + } + + return new GridFsResource[] { getResource(locationPattern) }; + } + + private DBObject getMappedQuery(Query query) { + return query == null ? null : queryMapper.getMappedObject(query.getQueryObject(), null); + } + + private GridFS getGridFs() { + DB db = dbFactory.getDb(); + return bucket == null ? new GridFS(db) : new GridFS(db, bucket); + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/gridfs/AntPathUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/gridfs/AntPathUnitTests.java new file mode 100644 index 0000000000..942d6821ed --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/gridfs/AntPathUnitTests.java @@ -0,0 +1,42 @@ +/* + * Copyright 2011 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.gridfs; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import java.util.regex.Pattern; + +import org.junit.Test; + +/** + * Unit tests for {@link AntPath}. + * + * @author Oliver Gierke + */ +public class AntPathUnitTests { + + @Test + public void buildRegexCorrectly() { + + AntPath path = new AntPath("**/foo/*-bar.xml"); + String regex = path.toRegex(); + + assertThat(Pattern.matches(regex, "foo/bar/foo/foo-bar.xml"), is(true)); + assertThat(Pattern.matches(regex, "foo/bar/foo/bar/foo-bar.xml"), is(false)); + assertThat(regex, is(".*\\Q/foo/\\E[^/]*\\Q-bar.xml\\E")); + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/gridfs/GridFsTemplateIIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/gridfs/GridFsTemplateIIntegrationTests.java new file mode 100644 index 0000000000..271fdf5b45 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/gridfs/GridFsTemplateIIntegrationTests.java @@ -0,0 +1,127 @@ +/* + * Copyright 2011 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.gridfs; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import static org.springframework.data.mongodb.core.query.Query.*; +import static org.springframework.data.mongodb.gridfs.GridFsCriteria.*; + +import java.io.IOException; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; +import com.mongodb.gridfs.GridFSDBFile; +import com.mongodb.gridfs.GridFSFile; + +/** + * Integration tests for {@link GridFsTemplate}. + * + * @author Oliver Gierke + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration("classpath:gridfs/gridfs.xml") +public class GridFsTemplateIIntegrationTests { + + Resource resource = new ClassPathResource("gridfs/gridfs.xml"); + + @Autowired + GridFsOperations operations; + + @Before + public void setUp() { + operations.delete(null); + } + + @Test + public void storesAndFindsSimpleDocument() throws IOException { + + GridFSFile reference = operations.store(resource.getInputStream(), "foo.xml"); + + List result = operations.find(null); + assertThat(result.size(), is(1)); + assertSame(result.get(0), reference); + } + + @Test + public void writesMetadataCorrectly() throws IOException { + + DBObject metadata = new BasicDBObject("key", "value"); + GridFSFile reference = operations.store(resource.getInputStream(), "foo.xml", metadata); + + List result = operations.find(query(whereMetaData("key").is("value"))); + assertThat(result.size(), is(1)); + assertSame(result.get(0), reference); + } + + @Test + public void marshalsComplexMetadata() throws IOException { + + Metadata metadata = new Metadata(); + metadata.version = "1.0"; + + GridFSFile reference = operations.store(resource.getInputStream(), "foo.xml", metadata); + List result = operations.find(query(whereFilename().is("foo.xml"))); + assertThat(result.size(), is(1)); + assertSame(result.get(0), reference); + } + + @Test + public void findsFilesByResourcePattern() throws IOException { + + GridFSFile reference = operations.store(resource.getInputStream(), "foo.xml"); + + GridFsResource[] resources = operations.getResources("*.xml"); + assertThat(resources.length, is(1)); + assertThat(resources[0].getId(), is(reference.getId())); + assertThat(resources[0].contentLength(), is(reference.getLength())); + assertThat(resources[0].getContentType(), is(reference.getContentType())); + } + + @Test + public void findsFilesByResourceLocation() throws IOException { + + GridFSFile reference = operations.store(resource.getInputStream(), "foo.xml"); + + GridFsResource[] resources = operations.getResources("foo.xml"); + assertThat(resources.length, is(1)); + assertThat(resources[0].getId(), is(reference.getId())); + assertThat(resources[0].contentLength(), is(reference.getLength())); + assertThat(resources[0].getContentType(), is(reference.getContentType())); + } + + private static void assertSame(GridFSFile left, GridFSFile right) { + + assertThat(left.getId(), is(right.getId())); + assertThat(left.getMD5(), is(right.getMD5())); + assertThat(left.getMetaData(), is(right.getMetaData())); + } + + class Metadata { + + String version; + } +} diff --git a/spring-data-mongodb/src/test/resources/gridfs/gridfs.xml b/spring-data-mongodb/src/test/resources/gridfs/gridfs.xml new file mode 100644 index 0000000000..4fef73f55d --- /dev/null +++ b/spring-data-mongodb/src/test/resources/gridfs/gridfs.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + +