DATAGRAPH-257 Fixed QueryResultBuilder cannot be cast to java.util.Set in NodeEntity for SDN 3.1+ context
This commit is contained in:
committed by
Michael Hunger
parent
04921b8e8d
commit
70e20a846c
@@ -25,8 +25,16 @@ import java.util.*;
|
||||
* @since 11.11.11
|
||||
*/
|
||||
public class ContainerConverter {
|
||||
/*
|
||||
|
||||
if (Set.class.isAssignableFrom(collectionLikeType)) return (C)IteratorUtil.addToCollection(this, new LinkedHashSet());
|
||||
if (Collection.class.isAssignableFrom(collectionLikeType)) return (C)IteratorUtil.addToCollection(this,new ArrayList());
|
||||
return (C)this;
|
||||
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T, C extends Iterable<T>> C toContainer(Class<C> container, Iterable<T> data) {
|
||||
if (container.isInstance(data)) return (C) data;
|
||||
if (Iterable.class.equals(container)) return (C) data;
|
||||
if (SortedSet.class.isAssignableFrom(container)) return (C) IteratorUtil.addToCollection(data, new TreeSet<T>());
|
||||
if (Set.class.isAssignableFrom(container)) return (C) IteratorUtil.addToCollection(data, new HashSet<T>());
|
||||
|
||||
@@ -20,14 +20,12 @@ import org.neo4j.graphdb.index.IndexHits;
|
||||
import org.neo4j.helpers.collection.ClosableIterable;
|
||||
import org.neo4j.helpers.collection.IterableWrapper;
|
||||
import org.neo4j.helpers.collection.IteratorUtil;
|
||||
import org.neo4j.helpers.collection.IteratorWrapper;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Slice;
|
||||
import org.springframework.data.neo4j.mapping.MappingPolicy;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* @author mh
|
||||
|
||||
@@ -19,13 +19,11 @@ package org.springframework.data.neo4j.fieldaccess;
|
||||
import org.neo4j.graphdb.Node;
|
||||
import org.springframework.dao.InvalidDataAccessApiUsageException;
|
||||
import org.springframework.data.neo4j.annotation.Query;
|
||||
|
||||
import org.springframework.data.neo4j.conversion.Result;
|
||||
import org.springframework.data.neo4j.mapping.MappingPolicy;
|
||||
import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty;
|
||||
import org.springframework.data.neo4j.support.Neo4jTemplate;
|
||||
import org.springframework.data.neo4j.support.query.CypherQueryEngine;
|
||||
import org.springframework.data.neo4j.support.query.QueryEngine;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
|
||||
import java.util.HashMap;
|
||||
@@ -106,8 +104,10 @@ public class QueryFieldAccessorFactory implements FieldAccessorFactory {
|
||||
if (actualType.isMap()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (typeInformation.isCollectionLike()) {
|
||||
return result.to(targetType);
|
||||
Class<? extends Iterable> collectionType = (Class<? extends Iterable>) typeInformation.getType();
|
||||
return result.to(targetType).as(collectionType);
|
||||
}
|
||||
return result.to(targetType).singleOrNull();
|
||||
}
|
||||
|
||||
@@ -149,19 +149,10 @@ public class GraphQueryMethod extends QueryMethod {
|
||||
return new CypherGraphRepositoryQuery(this, template);
|
||||
}
|
||||
|
||||
public boolean isSetResult() {
|
||||
final Class<Set> superClass = Set.class;
|
||||
return hasResultOfType(superClass);
|
||||
}
|
||||
|
||||
public boolean hasResultOfType(Class<?> superClass) {
|
||||
return superClass.isAssignableFrom(getReturnType());
|
||||
}
|
||||
|
||||
public boolean isCollectionResult() {
|
||||
return hasResultOfType(Collection.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Repository-Graph-Query-Method for "+method;
|
||||
|
||||
@@ -21,6 +21,7 @@ import org.neo4j.helpers.collection.IteratorUtil;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.SliceImpl;
|
||||
import org.springframework.data.neo4j.conversion.QueryResultBuilder;
|
||||
import org.springframework.data.neo4j.conversion.Result;
|
||||
import org.springframework.data.neo4j.core.GraphDatabase;
|
||||
import org.springframework.data.neo4j.support.Neo4jTemplate;
|
||||
@@ -103,18 +104,19 @@ abstract class GraphRepositoryQuery implements RepositoryQuery, ParameterResolve
|
||||
GraphQueryMethod queryMethod = getQueryMethod();
|
||||
final QueryEngine<?> queryEngine = getQueryEngine();
|
||||
final Class<?> compoundType = queryMethod.getCompoundType();
|
||||
Result<?> queryResult = queryEngine.query(queryString, params);
|
||||
Class<?> returnType = queryMethod.getReturnType();
|
||||
if (queryMethod.isPageQuery() || queryMethod.isSliceQuery()) {
|
||||
@SuppressWarnings("unchecked") final Iterable<?> result = queryEngine.query(queryString, params).to(compoundType);
|
||||
@SuppressWarnings("unchecked") final Iterable<?> result = queryResult.to(compoundType);
|
||||
Long count = computeCount(params);
|
||||
return createPage(result, accessor.getPageable(),count, queryMethod.isPageQuery());
|
||||
return createPage(result, accessor.getPageable(), count, queryMethod.isPageQuery());
|
||||
}
|
||||
|
||||
if (queryMethod.isIterableResult()) {
|
||||
final Result<?> result = queryEngine.query(queryString, params).to(compoundType);
|
||||
if (queryMethod.isSetResult()) return IteratorUtil.addToCollection(result,new LinkedHashSet());
|
||||
if (queryMethod.isCollectionResult()) return IteratorUtil.addToCollection(result,new ArrayList());
|
||||
return result;
|
||||
Class<? extends Iterable> collectionType = (Class<? extends Iterable>) returnType;
|
||||
return queryResult.to(compoundType).as(collectionType);
|
||||
}
|
||||
return queryEngine.query(queryString, params).to(queryMethod.getReturnType()).singleOrNull();
|
||||
return queryResult.to(returnType).singleOrNull();
|
||||
}
|
||||
|
||||
private Long computeCount(Map<String, Object> params) {
|
||||
|
||||
@@ -334,8 +334,18 @@ class Neo4jPersistentPropertyImpl extends AnnotationBasedPersistentProperty<Neo4
|
||||
}
|
||||
|
||||
public boolean isTransient() {
|
||||
|
||||
if (super.isTransient()) {
|
||||
|
||||
boolean primaryIsTransient = super.isTransient();
|
||||
// DATAGRAPH-257
|
||||
// We need to ignore @Query annotated fields for the moment. Entities should be able
|
||||
// to be marked with the Java transient modified so that they are not serialized,
|
||||
// however the still need @Query functionality to be able to execute and this will
|
||||
// not be done if the property is marked as transient
|
||||
if (field != null && field.isAnnotationPresent(Query.class)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (primaryIsTransient) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
package org.springframework.data.neo4j.entity;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.neo4j.model.Group;
|
||||
import org.springframework.data.neo4j.model.Person;
|
||||
import org.springframework.data.neo4j.repositories.FriendshipRepository;
|
||||
import org.springframework.data.neo4j.repositories.GroupRepository;
|
||||
import org.springframework.data.neo4j.repositories.PersonRepository;
|
||||
import org.springframework.data.neo4j.repository.*;
|
||||
import org.springframework.data.neo4j.support.Neo4jTemplate;
|
||||
import org.springframework.data.neo4j.support.node.Neo4jHelper;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.CleanContextCacheTestExecutionListener;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.TestExecutionListeners;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
|
||||
import org.springframework.test.context.transaction.BeforeTransaction;
|
||||
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.TransactionStatus;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
|
||||
import static junit.framework.Assert.assertNull;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.hasItems;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.neo4j.helpers.collection.IteratorUtil.asCollection;
|
||||
|
||||
|
||||
/**
|
||||
* These tests are all focused on testing @Query annotated fields on entities.
|
||||
*
|
||||
* @author Nicki Watt
|
||||
* @since 08.06.2014
|
||||
*/
|
||||
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@ContextConfiguration
|
||||
@TestExecutionListeners({CleanContextCacheTestExecutionListener.class, DependencyInjectionTestExecutionListener.class, TransactionalTestExecutionListener.class})
|
||||
public class EntityQueryTests {
|
||||
|
||||
@Autowired
|
||||
private Neo4jTemplate neo4jTemplate;
|
||||
|
||||
@Autowired
|
||||
private PersonRepository personRepository;
|
||||
@Autowired
|
||||
GroupRepository groupRepository;
|
||||
@Autowired
|
||||
FriendshipRepository friendshipRepository;
|
||||
@Autowired
|
||||
private PlatformTransactionManager transactionManager;
|
||||
|
||||
private TestTeam testTeam;
|
||||
private MatrixTeam matrixTeam;
|
||||
|
||||
@BeforeTransaction
|
||||
public void cleanDb() {
|
||||
Neo4jHelper.cleanDb(neo4jTemplate);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
testTeam = new TestTeam();
|
||||
matrixTeam = new MatrixTeam();
|
||||
new TransactionTemplate(transactionManager).execute(new TransactionCallbackWithoutResult() {
|
||||
@Override
|
||||
protected void doInTransactionWithoutResult(TransactionStatus status) {
|
||||
personRepository.deleteAll();
|
||||
groupRepository.deleteAll();
|
||||
friendshipRepository.deleteAll();
|
||||
testTeam.createSDGTeam(personRepository, groupRepository, friendshipRepository);
|
||||
matrixTeam.createMatrixTeam(personRepository, groupRepository, friendshipRepository);
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// DATAGRAPH-257
|
||||
@Transactional
|
||||
@Test
|
||||
public void testPropertyQueryOfTypeSetPopulatedCorrectly() {
|
||||
Group grp = neo4jTemplate.findOne(matrixTeam.matrixGroup.getId(), Group.class);
|
||||
Set<Person> members = grp.getTeamMembersAsSetViaQuery();
|
||||
assertThat( members, hasSize(3));
|
||||
assertThat( members, hasItems( matrixTeam.neo, matrixTeam.trinity, matrixTeam.cypher));
|
||||
|
||||
assertNull(members.iterator().next().getName());
|
||||
}
|
||||
|
||||
// DATAGRAPH-257
|
||||
@Transactional
|
||||
@Test
|
||||
public void testPropertyQueryOfTypeIterablePopulatedCorrectly() {
|
||||
Group grp = neo4jTemplate.findOne(matrixTeam.matrixGroup.getId(), Group.class);
|
||||
Iterable<Person> itMembers = grp.getTeamMembersAsIterableViaQuery();
|
||||
assertNotNull(itMembers);
|
||||
Collection<Person> members = asCollection(itMembers);
|
||||
assertThat( members, hasSize(3));
|
||||
assertThat( members, hasItems( matrixTeam.neo, matrixTeam.trinity, matrixTeam.cypher));
|
||||
assertNull(members.iterator().next().getName());
|
||||
}
|
||||
|
||||
// DATAGRAPH-257
|
||||
@Transactional
|
||||
@Test
|
||||
public void testRepositoryQueryOfTypeSetPopulatedCorrectly() {
|
||||
|
||||
// This is really for comparison purposes - comparing query via
|
||||
// a repository vs via an entity property with @Fetch i.e
|
||||
// testFetchedPropertyQueryOfTypeSetPopulatedCorrectly
|
||||
// (theoretically, they should return the same result)
|
||||
Set<Person> members = groupRepository.getTeamMembersAsSetViaQuery(matrixTeam.matrixGroup.getName());
|
||||
assertExpectionsOfMatrixTeamSetResult(members);
|
||||
}
|
||||
|
||||
// DATAGRAPH-257
|
||||
@Transactional
|
||||
@Test
|
||||
@Ignore("@Fetch breaks Neo4jEntityConverterTests - investigation required")
|
||||
public void testFetchedPropertyQueryOfTypeIterablePopulatedCorrectly() {
|
||||
Group grp = neo4jTemplate.findOne(matrixTeam.matrixGroup.getId(), Group.class);
|
||||
Iterable<Person> itMembers = grp.getFetchedTeamMembersAsIterableViaQuery();
|
||||
Collection<Person> members = asCollection(itMembers);
|
||||
assertExpectionsOfMatrixTeamSetResult(members);
|
||||
}
|
||||
|
||||
// DATAGRAPH-257
|
||||
@Transactional
|
||||
@Test
|
||||
@Ignore("@Fetch breaks Neo4jEntityConverterTests - investigation required")
|
||||
public void testFetchedPropertyQueryOfTypeSetPopulatedCorrectly() {
|
||||
Group grp = neo4jTemplate.findOne(matrixTeam.matrixGroup.getId(), Group.class);
|
||||
Set<Person> members = grp.getFetchedTeamMembersAsSetViaQuery();
|
||||
assertExpectionsOfMatrixTeamSetResult(members);
|
||||
}
|
||||
|
||||
private void assertExpectionsOfMatrixTeamSetResult(Collection<Person> members) {
|
||||
assertThat( members, hasSize(3));
|
||||
assertThat( members, hasItems( matrixTeam.neo, matrixTeam.trinity, matrixTeam.cypher));
|
||||
// Queries should return the fully populated object (at least at the first level)
|
||||
assertNotNull(members.iterator().next().getName());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -230,4 +230,39 @@ public class Group implements IGroup , Serializable {
|
||||
public Collection<Person> getFetchedPersons() {
|
||||
return fetchedPersons;
|
||||
}
|
||||
|
||||
/*
|
||||
Note: All @Query annotated fields should be transient so that they are not serialized
|
||||
*/
|
||||
|
||||
@Query("match (g2:g)-[:persons]->(member) where id(g2) = {self} return member")
|
||||
private transient Set<Person> teamMembersAsSetViaQuery;
|
||||
|
||||
@Query("match (g3:g)-[:persons]->(member) where id(g3) = {self} return member")
|
||||
private transient Iterable<Person> teamMembersAsIterableViaQuery;
|
||||
|
||||
//@Fetch //(breaks Neo4jEntityConverterTests - investigate)
|
||||
@Query("match (g4)-[:persons]->(member) where id(g4) = {self} return member")
|
||||
private transient Iterable<Person> fetchedTeamMembersAsIterableViaQuery;
|
||||
|
||||
//@Fetch //(breaks Neo4jEntityConverterTests - investigate)
|
||||
@Query("match (g1)-[:persons]-(member1) where id(g1) = {self} return member1")
|
||||
private transient Set<Person> fetchedTeamMembersAsSetViaQuery;
|
||||
|
||||
public Iterable<Person> getFetchedTeamMembersAsIterableViaQuery() {
|
||||
return fetchedTeamMembersAsIterableViaQuery;
|
||||
}
|
||||
|
||||
public Iterable<Person> getTeamMembersAsIterableViaQuery() {
|
||||
return teamMembersAsIterableViaQuery;
|
||||
}
|
||||
|
||||
public Set<Person> getTeamMembersAsSetViaQuery() {
|
||||
return teamMembersAsSetViaQuery;
|
||||
}
|
||||
|
||||
public Set<Person> getFetchedTeamMembersAsSetViaQuery() {
|
||||
return fetchedTeamMembersAsSetViaQuery;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -87,21 +87,20 @@ public class Person implements Being , Serializable {
|
||||
@RelatedToVia(type = "knows", elementClass = Friendship.class)
|
||||
private Iterable<Friendship> friendships;
|
||||
|
||||
@Query("start person=node({self}) match (person)<-[?:boss]-(boss) return boss")
|
||||
/*
|
||||
Note: All @Query annotated fields should be transient so that they are not serialized
|
||||
*/
|
||||
@Query("start person=node({self}) OPTIONAL MATCH (person)<-[:boss]-(boss) return boss")
|
||||
transient private Person bossByQuery;
|
||||
// NW - all queries should be transient
|
||||
|
||||
@Query("start person=node({self}) match (person)<-[?:boss]-(boss) return boss.name")
|
||||
@Query("start person=node({self}) OPTIONAL MATCH (person)<-[:boss]-(boss) return boss.name")
|
||||
transient private String bossName;
|
||||
// NW - all queries should be transient
|
||||
|
||||
@Query("start person=node({self}) match (person)<-[:persons]-(team)-[:persons]->(member) return member")
|
||||
transient private Iterable<Person> otherTeamMembers;
|
||||
// NW - all queries should be transient
|
||||
|
||||
@Query("start person=node({self}) match (person)<-[:persons]-(team)-[:persons]->(member) return member.name?, member.age?")
|
||||
@Query("start person=node({self}) match (person)<-[:persons]-(team)-[:persons]->(member) return member.name, member.age")
|
||||
transient private Iterable<Map<String,Object>> otherTeamMemberData;
|
||||
// NW - all queries should be transient
|
||||
|
||||
@RelatedTo(elementClass = Group.class, type = "interface_test", direction = Direction.OUTGOING)
|
||||
private Set<IGroup> groups;
|
||||
|
||||
@@ -18,10 +18,14 @@ package org.springframework.data.neo4j.repositories;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.neo4j.annotation.Query;
|
||||
import org.springframework.data.neo4j.model.Group;
|
||||
import org.springframework.data.neo4j.model.Person;
|
||||
import org.springframework.data.neo4j.repository.GraphRepository;
|
||||
import org.springframework.data.neo4j.repository.NamedIndexRepository;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
|
||||
/**
|
||||
* @author mh
|
||||
@@ -30,4 +34,7 @@ import org.springframework.data.neo4j.repository.NamedIndexRepository;
|
||||
public interface GroupRepository extends GraphRepository<Group>, NamedIndexRepository<Group> {
|
||||
Iterable<Group> findByFullTextNameLike(String name);
|
||||
Page<Group> findByName(String name, Pageable page);
|
||||
|
||||
@Query("match (group:g{name:{0}})-[:persons]->(aMember) return aMember")
|
||||
Set<Person> getTeamMembersAsSetViaQuery(String groupName);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 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.neo4j.repository;
|
||||
|
||||
import org.springframework.data.neo4j.model.Friendship;
|
||||
import org.springframework.data.neo4j.model.Group;
|
||||
import org.springframework.data.neo4j.model.Person;
|
||||
import org.springframework.data.neo4j.model.Personality;
|
||||
import org.springframework.data.neo4j.template.Neo4jOperations;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* @author Nicki Watt
|
||||
* @since 02.09.2013
|
||||
*/
|
||||
public class MatrixTeam {
|
||||
public Person neo;
|
||||
public Person cypher;
|
||||
public Person trinity;
|
||||
public Group matrixGroup;
|
||||
|
||||
public MatrixTeam() {
|
||||
}
|
||||
|
||||
public MatrixTeam createMatrixTeam(GraphRepository<Person> repo, GraphRepository<Group> groupRepo, GraphRepository<Friendship> friendshipRepository) {
|
||||
cypher = new Person("Cypher", 30);
|
||||
neo = new Person("Neo", 36);
|
||||
neo.setPersonality(Personality.EXTROVERT);
|
||||
neo.setLocation(16, 56);
|
||||
|
||||
trinity = new Person("Trinity", 25);
|
||||
trinity.setBoss(cypher);
|
||||
trinity.setLocation( 16.5, 56.5 );
|
||||
matrixGroup = new Group();
|
||||
matrixGroup.setName("Matrix");
|
||||
matrixGroup.addPerson(neo);
|
||||
matrixGroup.addPerson(cypher);
|
||||
matrixGroup.addPerson(trinity);
|
||||
|
||||
repo.save(Arrays.asList(cypher, trinity, neo));
|
||||
groupRepo.save(matrixGroup);
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:context="http://www.springframework.org/schema/context"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:neo4j="http://www.springframework.org/schema/data/neo4j"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
|
||||
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
|
||||
http://www.springframework.org/schema/data/neo4j http://www.springframework.org/schema/data/neo4j/spring-neo4j.xsd">
|
||||
<context:annotation-config/>
|
||||
<neo4j:config graphDatabaseService="graphDatabaseService" base-package="org.springframework.data.neo4j.model"/>
|
||||
<neo4j:repositories base-package="org.springframework.data.neo4j.repositories"/>
|
||||
<bean id="graphDatabaseService" class="org.neo4j.test.ImpermanentGraphDatabase" destroy-method="shutdown"/>
|
||||
</beans>
|
||||
Reference in New Issue
Block a user