DATADOC-22 - Added pagination and sorting to repository and finders.
- added CursorCallback to be able to apply limit, skip and sorting to the query execution - added paged execution for queries and extended SimpleMongoRepository to support paging and sorting as well - minor JavaDoc polishing.
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2002-2010 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.document.mongodb;
|
||||
|
||||
import com.mongodb.DBCursor;
|
||||
|
||||
|
||||
/**
|
||||
* Simple callback interface to allow customization of a {@link DBCursor}.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
public interface CursorPreparer {
|
||||
|
||||
/**
|
||||
* Prepare the given cursor (apply limits, skips and so on).
|
||||
*
|
||||
* @param cursor
|
||||
*/
|
||||
void prepare(DBCursor cursor);
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import com.mongodb.BasicDBObject;
|
||||
import com.mongodb.CommandResult;
|
||||
import com.mongodb.DB;
|
||||
import com.mongodb.DBCollection;
|
||||
import com.mongodb.DBCursor;
|
||||
import com.mongodb.DBObject;
|
||||
import com.mongodb.Mongo;
|
||||
import com.mongodb.MongoException;
|
||||
@@ -142,9 +143,37 @@ public class MongoTemplate implements InitializingBean {
|
||||
return action.doInDB(db);
|
||||
} catch (MongoException e) {
|
||||
throw MongoDbUtils.translateMongoExceptionIfPossible(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Executes the given {@link CollectionCallback} on the default collection.
|
||||
*
|
||||
* @param <T>
|
||||
* @param callback
|
||||
* @return
|
||||
*/
|
||||
public <T> T execute(CollectionCallback<T> callback) {
|
||||
return execute(callback, defaultCollectionName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the given {@link CollectionCallback} on the collection of the given name.
|
||||
*
|
||||
* @param <T>
|
||||
* @param callback
|
||||
* @param collectionName
|
||||
* @return
|
||||
*/
|
||||
public <T> T execute(CollectionCallback<T> callback, String collectionName) {
|
||||
|
||||
try {
|
||||
return callback.doInCollection(getCollection(collectionName));
|
||||
} catch (MongoException e) {
|
||||
throw MongoDbUtils.translateMongoExceptionIfPossible(e);
|
||||
}
|
||||
}
|
||||
|
||||
public <T> T executeInSession(DBCallback<T> action) {
|
||||
DB db = getDb();
|
||||
db.requestStart();
|
||||
@@ -339,14 +368,26 @@ public class MongoTemplate implements InitializingBean {
|
||||
return query(getDefaultCollectionName(), query, targetClass); //
|
||||
}
|
||||
|
||||
public <T> List<T> query(DBObject query, Class<T> targetClass, CursorPreparer preparer) {
|
||||
return query(getDefaultCollectionName(), query, targetClass, preparer); //
|
||||
}
|
||||
|
||||
public <T> List<T> query(DBObject query, Class<T> targetClass, MongoReader<T> reader) {
|
||||
return query(getDefaultCollectionName(), query, targetClass, reader);
|
||||
}
|
||||
|
||||
public <T> List<T> query(String collectionName, DBObject query, Class<T> targetClass) {
|
||||
return query(collectionName, query, targetClass, (CursorPreparer) null);
|
||||
}
|
||||
|
||||
public <T> List<T> query(String collectionName, DBObject query, Class<T> targetClass, CursorPreparer preparer) {
|
||||
DBCollection collection = getDb().getCollection(collectionName);
|
||||
List<T> results = new ArrayList<T>();
|
||||
for (DBObject dbo : collection.find(query)) {
|
||||
DBCursor cursor = collection.find(query);
|
||||
if (preparer != null) {
|
||||
preparer.prepare(cursor);
|
||||
}
|
||||
for (DBObject dbo : cursor) {
|
||||
Object obj = mongoConverter.read(targetClass,dbo);
|
||||
//effectively acts as a query on the collection restricting it to elements of a specific type
|
||||
if (targetClass.isInstance(obj)) {
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* Copyright 2010 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.document.mongodb.repository;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.data.document.mongodb.CursorPreparer;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.domain.Sort.Direction;
|
||||
import org.springframework.data.domain.Sort.Order;
|
||||
|
||||
import com.mongodb.BasicDBObject;
|
||||
import com.mongodb.DBCursor;
|
||||
|
||||
|
||||
/**
|
||||
* Collection of utility methods to apply sorting and pagination to a
|
||||
* {@link DBCursor}.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
class MongoCursorUtils {
|
||||
|
||||
/**
|
||||
* Creates a {@link CursorPreparer} applying the given {@link Pageable} to
|
||||
* the cursor.
|
||||
*
|
||||
* @param pageable
|
||||
* @return
|
||||
*/
|
||||
public static CursorPreparer withPagination(Pageable pageable) {
|
||||
|
||||
return new PaginationCursorPreparer(pageable);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a {@link CursorPreparer} to apply the given {@link Sort} to the
|
||||
* cursor.
|
||||
*
|
||||
* @param sort
|
||||
* @return
|
||||
*/
|
||||
public static CursorPreparer withSorting(Sort sort) {
|
||||
|
||||
return new SortingCursorPreparer(sort);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the given {@link Pageable} to the given {@link DBCursor}.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
private static class PaginationCursorPreparer implements CursorPreparer {
|
||||
|
||||
private final Pageable pageable;
|
||||
private final SortingCursorPreparer sortingPreparer;
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new {@link PaginationCursorPreparer}.
|
||||
*
|
||||
* @param pageable
|
||||
*/
|
||||
public PaginationCursorPreparer(Pageable pageable) {
|
||||
|
||||
this.pageable = pageable;
|
||||
this.sortingPreparer =
|
||||
new SortingCursorPreparer(pageable.getSort());
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see
|
||||
* org.springframework.data.document.mongodb.CursorPreparer#prepare(
|
||||
* com.mongodb.DBCursor)
|
||||
*/
|
||||
public void prepare(DBCursor cursor) {
|
||||
|
||||
if (pageable == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int toSkip = pageable.getPageSize() * pageable.getPageNumber();
|
||||
int first = pageable.getPageSize();
|
||||
|
||||
cursor.limit(first).skip(toSkip);
|
||||
sortingPreparer.prepare(cursor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the given {@link Sort} to the given {@link DBCursor}.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
private static class SortingCursorPreparer implements CursorPreparer {
|
||||
|
||||
private final Sort sort;
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new {@link SortingCursorPreparer}.
|
||||
*
|
||||
* @param sort
|
||||
*/
|
||||
public SortingCursorPreparer(Sort sort) {
|
||||
|
||||
this.sort = sort;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see
|
||||
* org.springframework.data.document.mongodb.CursorPreparer#prepare(
|
||||
* com.mongodb.DBCursor)
|
||||
*/
|
||||
public void prepare(DBCursor cursor) {
|
||||
|
||||
if (sort == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, Integer> sorts = new HashMap<String, Integer>();
|
||||
|
||||
for (Order order : sort) {
|
||||
sorts.put(order.getProperty(),
|
||||
Direction.ASC.equals(order.getDirection()) ? 1 : -1);
|
||||
}
|
||||
|
||||
cursor.sort(new BasicDBObject(sorts));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,15 +15,22 @@
|
||||
*/
|
||||
package org.springframework.data.document.mongodb.repository;
|
||||
|
||||
import static org.springframework.data.document.mongodb.repository.MongoCursorUtils.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.document.mongodb.CollectionCallback;
|
||||
import org.springframework.data.document.mongodb.MongoTemplate;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.repository.query.QueryMethod;
|
||||
import org.springframework.data.repository.query.RepositoryQuery;
|
||||
import org.springframework.data.repository.query.SimpleParameterAccessor;
|
||||
import org.springframework.data.repository.query.parser.PartTree;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.mongodb.DBCollection;
|
||||
import com.mongodb.DBCursor;
|
||||
import com.mongodb.DBObject;
|
||||
|
||||
|
||||
@@ -50,8 +57,6 @@ public class MongoQuery implements RepositoryQuery {
|
||||
|
||||
Assert.notNull(template);
|
||||
Assert.notNull(method);
|
||||
Assert.isTrue(!method.isPageQuery(),
|
||||
"Pagination queries not supported!");
|
||||
|
||||
this.method = method;
|
||||
this.template = template;
|
||||
@@ -73,13 +78,106 @@ public class MongoQuery implements RepositoryQuery {
|
||||
MongoQueryCreator creator = new MongoQueryCreator(tree, accessor);
|
||||
DBObject query = creator.createQuery();
|
||||
|
||||
List<?> result =
|
||||
template.query(template.getDefaultCollectionName(), query,
|
||||
method.getDomainClass());
|
||||
|
||||
if (method.isCollectionQuery()) {
|
||||
return result;
|
||||
return new CollectionExecution().execute(query);
|
||||
} else if (method.isPageQuery()) {
|
||||
return new PagedExecution(creator, accessor.getPageable())
|
||||
.execute(query);
|
||||
} else {
|
||||
return new SingleEntityExecution().execute(query);
|
||||
}
|
||||
}
|
||||
|
||||
private abstract class Execution {
|
||||
|
||||
abstract Object execute(DBObject query);
|
||||
|
||||
|
||||
protected List<?> readCollection(DBObject query) {
|
||||
|
||||
return template.query(template.getDefaultCollectionName(), query,
|
||||
method.getDomainClass());
|
||||
}
|
||||
}
|
||||
|
||||
class CollectionExecution extends Execution {
|
||||
|
||||
@Override
|
||||
public Object execute(DBObject query) {
|
||||
|
||||
return readCollection(query);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Execution} for pagination queries.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
class PagedExecution extends Execution {
|
||||
|
||||
private final Pageable pageable;
|
||||
private final MongoQueryCreator creator;
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new {@link PagedExecution}.
|
||||
*
|
||||
* @param pageable
|
||||
*/
|
||||
public PagedExecution(MongoQueryCreator creator, Pageable pageable) {
|
||||
|
||||
Assert.notNull(creator);
|
||||
Assert.notNull(pageable);
|
||||
this.creator = creator;
|
||||
this.pageable = pageable;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see
|
||||
* org.springframework.data.document.mongodb.repository.MongoQuery.Execution
|
||||
* #execute(com.mongodb.DBObject)
|
||||
*/
|
||||
@Override
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
Object execute(DBObject query) {
|
||||
|
||||
int count = getCollectionCursor(creator.createQuery()).count();
|
||||
List<?> result =
|
||||
template.query(query, method.getDomainClass(),
|
||||
withPagination(pageable));
|
||||
|
||||
return new PageImpl(result, pageable, count);
|
||||
}
|
||||
|
||||
|
||||
private DBCursor getCollectionCursor(final DBObject query) {
|
||||
|
||||
return template.execute(new CollectionCallback<DBCursor>() {
|
||||
|
||||
public DBCursor doInCollection(DBCollection collection) {
|
||||
|
||||
return collection.find(query);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Execution} to return a single entity.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
class SingleEntityExecution extends Execution {
|
||||
|
||||
@Override
|
||||
Object execute(DBObject query) {
|
||||
|
||||
List<?> result = readCollection(query);
|
||||
|
||||
return result.isEmpty() ? null : result.get(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ package org.springframework.data.document.mongodb.repository;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.springframework.data.repository.PagingAndSortingRepository;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
|
||||
@@ -26,6 +27,6 @@ import org.springframework.data.repository.Repository;
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
public interface MongoRepository<T, ID extends Serializable> extends
|
||||
Repository<T, ID> {
|
||||
PagingAndSortingRepository<T, Serializable> {
|
||||
|
||||
}
|
||||
|
||||
@@ -15,15 +15,23 @@
|
||||
*/
|
||||
package org.springframework.data.document.mongodb.repository;
|
||||
|
||||
import static org.springframework.data.document.mongodb.repository.MongoCursorUtils.*;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.document.mongodb.MongoTemplate;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.repository.PagingAndSortingRepository;
|
||||
import org.springframework.data.repository.support.IsNewAware;
|
||||
import org.springframework.data.repository.support.RepositorySupport;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.mongodb.BasicDBObject;
|
||||
import com.mongodb.QueryBuilder;
|
||||
|
||||
|
||||
@@ -33,7 +41,7 @@ import com.mongodb.QueryBuilder;
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
public class SimpleMongoRepository<T, ID extends Serializable> extends
|
||||
RepositorySupport<T, ID> {
|
||||
RepositorySupport<T, ID> implements PagingAndSortingRepository<T, ID> {
|
||||
|
||||
private final MongoTemplate template;
|
||||
private MongoEntityInformation entityInformation;
|
||||
@@ -132,7 +140,8 @@ public class SimpleMongoRepository<T, ID extends Serializable> extends
|
||||
*/
|
||||
public Long count() {
|
||||
|
||||
return Long.valueOf(findAll().size());
|
||||
return template.getCollection(template.getDefaultCollectionName())
|
||||
.count();
|
||||
}
|
||||
|
||||
|
||||
@@ -176,6 +185,39 @@ public class SimpleMongoRepository<T, ID extends Serializable> extends
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see
|
||||
* org.springframework.data.repository.PagingAndSortingRepository#findAll
|
||||
* (org.springframework.data.domain.Pageable)
|
||||
*/
|
||||
public Page<T> findAll(final Pageable pageable) {
|
||||
|
||||
Long count = count();
|
||||
|
||||
List<T> list =
|
||||
template.query(new BasicDBObject(), getDomainClass(),
|
||||
withPagination(pageable));
|
||||
|
||||
return new PageImpl<T>(list, pageable, count);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see
|
||||
* org.springframework.data.repository.PagingAndSortingRepository#findAll
|
||||
* (org.springframework.data.domain.Sort)
|
||||
*/
|
||||
public List<T> findAll(final Sort sort) {
|
||||
|
||||
return template.query(new BasicDBObject(), getDomainClass(),
|
||||
withSorting(sort));
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
|
||||
@@ -32,7 +32,15 @@ public class Person {
|
||||
|
||||
public Person() {
|
||||
|
||||
this(null, null);
|
||||
}
|
||||
|
||||
|
||||
public Person(String firstname, String lastname) {
|
||||
|
||||
this.id = ObjectId.get().toString();
|
||||
this.firstname = firstname;
|
||||
this.lastname = lastname;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -17,6 +17,9 @@ package org.springframework.data.document.mongodb.repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
|
||||
|
||||
/**
|
||||
* Sample repository managing {@link Person} entities.
|
||||
@@ -25,8 +28,32 @@ import java.util.List;
|
||||
*/
|
||||
public interface PersonRepository extends MongoRepository<Person, Long> {
|
||||
|
||||
/**
|
||||
* Returns all {@link Person}s with the given lastname.
|
||||
*
|
||||
* @param lastname
|
||||
* @return
|
||||
*/
|
||||
List<Person> findByLastname(String lastname);
|
||||
|
||||
|
||||
/**
|
||||
* Returns all {@link Person}s with a firstname matching the given one
|
||||
* (*-wildcard supported).
|
||||
*
|
||||
* @param firstname
|
||||
* @return
|
||||
*/
|
||||
List<Person> findByFirstnameLike(String firstname);
|
||||
|
||||
|
||||
/**
|
||||
* Returns a page of {@link Person}s with a lastname mathing the given one
|
||||
* (*-wildcards supported).
|
||||
*
|
||||
* @param lastname
|
||||
* @param pageable
|
||||
* @return
|
||||
*/
|
||||
Page<Person> findByLastnameLike(String lastname, Pageable pageable);
|
||||
}
|
||||
|
||||
@@ -18,11 +18,16 @@ package org.springframework.data.document.mongodb.repository;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
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.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort.Direction;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
@@ -39,23 +44,63 @@ public class PersonRepositoryIntegrationTests {
|
||||
@Autowired
|
||||
PersonRepository repository;
|
||||
|
||||
Person dave, carter, boyd, stefan, leroi;
|
||||
|
||||
@Test
|
||||
public void createAndExecuteFinderRoundtrip() throws Exception {
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
|
||||
repository.deleteAll();
|
||||
|
||||
Person person = new Person();
|
||||
person.setFirstname("Oliver");
|
||||
person.setLastname("Gierke");
|
||||
person = repository.save(person);
|
||||
dave = new Person("Dave", "Matthews");
|
||||
carter = new Person("Carter", "Beauford");
|
||||
boyd = new Person("Boyd", "Tinsley");
|
||||
stefan = new Person("Stefan", "Lessard");
|
||||
leroi = new Person("Leroi", "Moore");
|
||||
|
||||
List<Person> result = repository.findByLastname("Gierke");
|
||||
assertThat(result.size(), is(1));
|
||||
assertThat(result, hasItem(person));
|
||||
repository.save(Arrays.asList(dave, carter, boyd, stefan, leroi));
|
||||
}
|
||||
|
||||
result = repository.findByFirstnameLike("Oli*");
|
||||
|
||||
@Test
|
||||
public void findsPersonsByLastname() throws Exception {
|
||||
|
||||
List<Person> result = repository.findByLastname("Beauford");
|
||||
assertThat(result.size(), is(1));
|
||||
assertThat(result, hasItem(person));
|
||||
assertThat(result, hasItem(carter));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void findsPersonsByFirstnameLike() throws Exception {
|
||||
|
||||
List<Person> result = repository.findByFirstnameLike("Bo*");
|
||||
assertThat(result.size(), is(1));
|
||||
assertThat(result, hasItem(boyd));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void findsPagedPersons() throws Exception {
|
||||
|
||||
Page<Person> result =
|
||||
repository.findAll(new PageRequest(1, 2, Direction.ASC,
|
||||
"lastname"));
|
||||
assertThat(result.isFirstPage(), is(false));
|
||||
assertThat(result.isLastPage(), is(false));
|
||||
assertThat(result, hasItems(dave, leroi));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void executesPagedFinderCorrectly() throws Exception {
|
||||
|
||||
Page<Person> page =
|
||||
repository.findByLastnameLike("*a*", new PageRequest(0, 2,
|
||||
Direction.ASC, "lastname"));
|
||||
assertThat(page.isFirstPage(), is(true));
|
||||
assertThat(page.isLastPage(), is(false));
|
||||
assertThat(page.getNumberOfElements(), is(2));
|
||||
assertThat(page, hasItems(carter, stefan));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user