experimental support for reactive Jakarta Data repositories
this time with uni-fied access to the M.SS Signed-off-by: Gavin King <gavin@hibernate.org>
This commit is contained in:
parent
d52edeb0e5
commit
c5f9ada2fc
|
@ -0,0 +1,106 @@
|
||||||
|
package org.hibernate.processor.test.data.reactive;
|
||||||
|
|
||||||
|
import io.smallrye.mutiny.Uni;
|
||||||
|
import jakarta.data.Limit;
|
||||||
|
import jakarta.data.Order;
|
||||||
|
import jakarta.data.Sort;
|
||||||
|
import jakarta.data.page.Page;
|
||||||
|
import jakarta.data.page.PageRequest;
|
||||||
|
import jakarta.data.repository.By;
|
||||||
|
import jakarta.data.repository.Delete;
|
||||||
|
import jakarta.data.repository.Find;
|
||||||
|
import jakarta.data.repository.Insert;
|
||||||
|
import jakarta.data.repository.OrderBy;
|
||||||
|
import jakarta.data.repository.Query;
|
||||||
|
import jakarta.data.repository.Repository;
|
||||||
|
import jakarta.data.repository.Save;
|
||||||
|
import jakarta.data.repository.Update;
|
||||||
|
import org.hibernate.reactive.mutiny.Mutiny;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface Library2 {
|
||||||
|
|
||||||
|
Uni<Mutiny.StatelessSession> session(); //required
|
||||||
|
|
||||||
|
@Find
|
||||||
|
Uni<Book> book(String isbn);
|
||||||
|
|
||||||
|
@Find
|
||||||
|
Uni<List<Book>> books(@By("isbn") List<String> isbns);
|
||||||
|
|
||||||
|
@Find
|
||||||
|
Uni<Book> book(String title, LocalDate publicationDate);
|
||||||
|
|
||||||
|
@Find
|
||||||
|
Uni<List<Book>> publications(Type type, Sort<Book> sort);
|
||||||
|
|
||||||
|
@Find
|
||||||
|
@OrderBy("title")
|
||||||
|
Uni<List<Book>> booksByPublisher(String publisher_name);
|
||||||
|
|
||||||
|
@Query("where title like :titlePattern")
|
||||||
|
@OrderBy("title")
|
||||||
|
Uni<List<Book>> booksByTitle(String titlePattern);
|
||||||
|
|
||||||
|
// not required by Jakarta Data
|
||||||
|
record BookWithAuthor(Book book, Author author) {}
|
||||||
|
@Query("select b, a from Book b join b.authors a order by b.isbn, a.ssn")
|
||||||
|
Uni<List<BookWithAuthor>> booksWithAuthors();
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
Uni<Void> create(Book book);
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
Uni<Void> create(Book[] book);
|
||||||
|
|
||||||
|
@Update
|
||||||
|
Uni<Void> update(Book book);
|
||||||
|
|
||||||
|
@Update
|
||||||
|
Uni<Void> update(Book[] books);
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
Uni<Void> delete(Book book);
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
Uni<Void> delete(Book[] book);
|
||||||
|
|
||||||
|
@Save
|
||||||
|
Uni<Void> upsert(Book book);
|
||||||
|
|
||||||
|
@Find
|
||||||
|
Uni<Author> author(String ssn);
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
Uni<Void> create(Author author);
|
||||||
|
|
||||||
|
@Update
|
||||||
|
Uni<Void> update(Author author);
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
Uni<Publisher[]> insertAll(Publisher[] publishers);
|
||||||
|
|
||||||
|
@Save
|
||||||
|
Uni<Publisher> save(Publisher publisher);
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
Uni<Publisher> delete(Publisher publisher);
|
||||||
|
|
||||||
|
@Find
|
||||||
|
@OrderBy("isbn")
|
||||||
|
Uni<Page<Book>> allBooks(PageRequest<Book> pageRequest);
|
||||||
|
|
||||||
|
@Find
|
||||||
|
@OrderBy("name")
|
||||||
|
@OrderBy("address.city")
|
||||||
|
Uni<List<Author>> allAuthors(Order<Author> order, Limit limit);
|
||||||
|
|
||||||
|
@Find
|
||||||
|
Uni<List<Author>> authorsByCity(@By("address.city") String city);
|
||||||
|
|
||||||
|
@Find
|
||||||
|
Uni<List<Author>> authorsByCityAndPostcode(String address_city, String address_postcode);
|
||||||
|
}
|
|
@ -18,13 +18,14 @@ import static org.hibernate.processor.test.util.TestUtil.getMetaModelSourceAsStr
|
||||||
*/
|
*/
|
||||||
public class ReactiveTest extends CompilationTest {
|
public class ReactiveTest extends CompilationTest {
|
||||||
@Test
|
@Test
|
||||||
@WithClasses({ Publisher.class, Author.class, Address.class, Book.class, Library.class })
|
@WithClasses({ Publisher.class, Author.class, Address.class, Book.class, Library.class, Library2.class })
|
||||||
public void test() {
|
public void test() {
|
||||||
System.out.println( getMetaModelSourceAsString( Author.class ) );
|
System.out.println( getMetaModelSourceAsString( Author.class ) );
|
||||||
System.out.println( getMetaModelSourceAsString( Book.class ) );
|
System.out.println( getMetaModelSourceAsString( Book.class ) );
|
||||||
System.out.println( getMetaModelSourceAsString( Author.class, true ) );
|
System.out.println( getMetaModelSourceAsString( Author.class, true ) );
|
||||||
System.out.println( getMetaModelSourceAsString( Book.class, true ) );
|
System.out.println( getMetaModelSourceAsString( Book.class, true ) );
|
||||||
System.out.println( getMetaModelSourceAsString( Library.class ) );
|
System.out.println( getMetaModelSourceAsString( Library.class ) );
|
||||||
|
System.out.println( getMetaModelSourceAsString( Library2.class ) );
|
||||||
assertMetamodelClassGeneratedFor( Author.class, true );
|
assertMetamodelClassGeneratedFor( Author.class, true );
|
||||||
assertMetamodelClassGeneratedFor( Book.class, true );
|
assertMetamodelClassGeneratedFor( Book.class, true );
|
||||||
assertMetamodelClassGeneratedFor( Publisher.class, true );
|
assertMetamodelClassGeneratedFor( Publisher.class, true );
|
||||||
|
@ -32,5 +33,6 @@ public class ReactiveTest extends CompilationTest {
|
||||||
assertMetamodelClassGeneratedFor( Book.class );
|
assertMetamodelClassGeneratedFor( Book.class );
|
||||||
assertMetamodelClassGeneratedFor( Publisher.class );
|
assertMetamodelClassGeneratedFor( Publisher.class );
|
||||||
assertMetamodelClassGeneratedFor( Library.class );
|
assertMetamodelClassGeneratedFor( Library.class );
|
||||||
|
assertMetamodelClassGeneratedFor( Library2.class );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -181,23 +181,26 @@ public abstract class AbstractQueryMethod implements MetaAttribute {
|
||||||
|
|
||||||
boolean isUsingStatelessSession() {
|
boolean isUsingStatelessSession() {
|
||||||
return HIB_STATELESS_SESSION.equals(sessionType)
|
return HIB_STATELESS_SESSION.equals(sessionType)
|
||||||
|| MUTINY_STATELESS_SESSION.equals(sessionType);
|
|| MUTINY_STATELESS_SESSION.equals(sessionType)
|
||||||
|
|| UNI_MUTINY_STATELESS_SESSION.equals(sessionType);
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isReactive() {
|
boolean isReactive() {
|
||||||
return MUTINY_SESSION.equals(sessionType)
|
return MUTINY_SESSION.equals(sessionType)
|
||||||
|| MUTINY_STATELESS_SESSION.equals(sessionType)
|
|| MUTINY_STATELESS_SESSION.equals(sessionType)
|
||||||
|| UNI_MUTINY_SESSION.equals(sessionType);
|
|| UNI_MUTINY_SESSION.equals(sessionType)
|
||||||
|
|| UNI_MUTINY_STATELESS_SESSION.equals(sessionType);
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isReactiveSession() {
|
boolean isReactiveSession() {
|
||||||
return UNI_MUTINY_SESSION.equals(sessionType);
|
return UNI_MUTINY_SESSION.equals(sessionType)
|
||||||
|
|| UNI_MUTINY_STATELESS_SESSION.equals(sessionType);
|
||||||
}
|
}
|
||||||
|
|
||||||
String localSessionName() {
|
String localSessionName() {
|
||||||
return isReactiveSession() ? "resolvedSession" : sessionName;
|
return isReactiveSession() ? "_session" : sessionName;
|
||||||
}
|
}
|
||||||
|
|
||||||
void chainSession(StringBuilder declaration) {
|
void chainSession(StringBuilder declaration) {
|
||||||
// Reactive calls always have a return type
|
// Reactive calls always have a return type
|
||||||
if ( isReactiveSession() ) {
|
if ( isReactiveSession() ) {
|
||||||
|
@ -206,7 +209,7 @@ public abstract class AbstractQueryMethod implements MetaAttribute {
|
||||||
.append(sessionName)
|
.append(sessionName)
|
||||||
.append(".chain(")
|
.append(".chain(")
|
||||||
.append(localSessionName())
|
.append(localSessionName())
|
||||||
.append(" -> {\n\t");
|
.append(" -> {\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -655,9 +658,14 @@ public abstract class AbstractQueryMethod implements MetaAttribute {
|
||||||
break;
|
break;
|
||||||
case JD_PAGE:
|
case JD_PAGE:
|
||||||
if ( isReactive() ) {
|
if ( isReactive() ) {
|
||||||
|
if ( returnTypeName == null ) {
|
||||||
|
throw new AssertionFailure("entity class cannot be null");
|
||||||
|
}
|
||||||
declaration
|
declaration
|
||||||
.append("\t\t\t.getResultList()\n")
|
.append("\t\t\t.getResultList()\n")
|
||||||
.append("\t\t\t.map(_results -> ");
|
.append("\t\t\t.map(_results -> (Page<")
|
||||||
|
.append(annotationMetaEntity.importType(returnTypeName))
|
||||||
|
.append(">)");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
declaration
|
declaration
|
||||||
|
@ -741,8 +749,8 @@ public abstract class AbstractQueryMethod implements MetaAttribute {
|
||||||
|
|
||||||
boolean isUnifiableReturnType(@Nullable String containerType) {
|
boolean isUnifiableReturnType(@Nullable String containerType) {
|
||||||
return containerType == null
|
return containerType == null
|
||||||
|| LIST.equals(containerType)
|
|| LIST.equals(containerType)
|
||||||
|| JD_PAGE.equals(containerType)
|
|| JD_PAGE.equals(containerType)
|
||||||
|| JD_CURSORED_PAGE.equals(containerType);
|
|| JD_CURSORED_PAGE.equals(containerType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1152,7 +1152,8 @@ public class AnnotationMetaEntity extends AnnotationMeta {
|
||||||
private void addLifecycleMethod(ExecutableElement method) {
|
private void addLifecycleMethod(ExecutableElement method) {
|
||||||
final TypeMirror returnType = ununi(method.getReturnType());
|
final TypeMirror returnType = ununi(method.getReturnType());
|
||||||
if ( !HIB_STATELESS_SESSION.equals(sessionType)
|
if ( !HIB_STATELESS_SESSION.equals(sessionType)
|
||||||
&& !MUTINY_STATELESS_SESSION.equals(sessionType) ) {
|
&& !MUTINY_STATELESS_SESSION.equals(sessionType)
|
||||||
|
&& !UNI_MUTINY_STATELESS_SESSION.equals(sessionType) ) {
|
||||||
context.message( method,
|
context.message( method,
|
||||||
"repository must be backed by a 'StatelessSession'",
|
"repository must be backed by a 'StatelessSession'",
|
||||||
Diagnostic.Kind.ERROR );
|
Diagnostic.Kind.ERROR );
|
||||||
|
@ -1593,6 +1594,8 @@ public class AnnotationMetaEntity extends AnnotationMeta {
|
||||||
case HIB_STATELESS_SESSION:
|
case HIB_STATELESS_SESSION:
|
||||||
case MUTINY_SESSION:
|
case MUTINY_SESSION:
|
||||||
case MUTINY_STATELESS_SESSION:
|
case MUTINY_STATELESS_SESSION:
|
||||||
|
// case UNI_MUTINY_SESSION:
|
||||||
|
// case UNI_MUTINY_STATELESS_SESSION:
|
||||||
return "session";
|
return "session";
|
||||||
default:
|
default:
|
||||||
return sessionGetter;
|
return sessionGetter;
|
||||||
|
@ -2622,11 +2625,13 @@ public class AnnotationMetaEntity extends AnnotationMeta {
|
||||||
private boolean usingReactiveSession(String sessionType) {
|
private boolean usingReactiveSession(String sessionType) {
|
||||||
return MUTINY_SESSION.equals(sessionType)
|
return MUTINY_SESSION.equals(sessionType)
|
||||||
|| MUTINY_STATELESS_SESSION.equals(sessionType)
|
|| MUTINY_STATELESS_SESSION.equals(sessionType)
|
||||||
|| UNI_MUTINY_SESSION.equals(sessionType);
|
|| UNI_MUTINY_SESSION.equals(sessionType)
|
||||||
|
|| UNI_MUTINY_STATELESS_SESSION.equals(sessionType);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean usingStatelessSession(String sessionType) {
|
private boolean usingStatelessSession(String sessionType) {
|
||||||
return HIB_STATELESS_SESSION.equals(sessionType)
|
return HIB_STATELESS_SESSION.equals(sessionType)
|
||||||
|| MUTINY_STATELESS_SESSION.equals(sessionType);
|
|| MUTINY_STATELESS_SESSION.equals(sessionType)
|
||||||
|
|| UNI_MUTINY_STATELESS_SESSION.equals(sessionType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ import static org.hibernate.processor.util.Constants.MUTINY_SESSION;
|
||||||
import static org.hibernate.processor.util.Constants.MUTINY_SESSION_FACTORY;
|
import static org.hibernate.processor.util.Constants.MUTINY_SESSION_FACTORY;
|
||||||
import static org.hibernate.processor.util.Constants.MUTINY_STATELESS_SESSION;
|
import static org.hibernate.processor.util.Constants.MUTINY_STATELESS_SESSION;
|
||||||
import static org.hibernate.processor.util.Constants.UNI_MUTINY_SESSION;
|
import static org.hibernate.processor.util.Constants.UNI_MUTINY_SESSION;
|
||||||
|
import static org.hibernate.processor.util.Constants.UNI_MUTINY_STATELESS_SESSION;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used by the container to instantiate a Jakarta Data repository.
|
* Used by the container to instantiate a Jakarta Data repository.
|
||||||
|
@ -51,8 +52,9 @@ public class DefaultConstructor implements MetaAttribute {
|
||||||
|
|
||||||
private boolean isReactive() {
|
private boolean isReactive() {
|
||||||
return MUTINY_SESSION.equals(sessionTypeName)
|
return MUTINY_SESSION.equals(sessionTypeName)
|
||||||
|| MUTINY_STATELESS_SESSION.equals(sessionTypeName)
|
|| MUTINY_STATELESS_SESSION.equals(sessionTypeName)
|
||||||
|| UNI_MUTINY_SESSION.equals(sessionTypeName);
|
|| UNI_MUTINY_SESSION.equals(sessionTypeName)
|
||||||
|
|| UNI_MUTINY_STATELESS_SESSION.equals(sessionTypeName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -96,19 +98,24 @@ public class DefaultConstructor implements MetaAttribute {
|
||||||
.append("Factory.unwrap(")
|
.append("Factory.unwrap(")
|
||||||
.append(annotationMetaEntity.importType(isReactive() ? MUTINY_SESSION_FACTORY : HIB_SESSION_FACTORY))
|
.append(annotationMetaEntity.importType(isReactive() ? MUTINY_SESSION_FACTORY : HIB_SESSION_FACTORY))
|
||||||
.append(".class).openStatelessSession()");
|
.append(".class).openStatelessSession()");
|
||||||
if ( isReactive() ) {
|
if ( MUTINY_SESSION.equals(sessionTypeName)
|
||||||
|
|| MUTINY_STATELESS_SESSION.equals(sessionTypeName) ) {
|
||||||
|
// this is crap
|
||||||
declaration
|
declaration
|
||||||
.append(".await().indefinitely()");
|
.append(".await().indefinitely()");
|
||||||
}
|
}
|
||||||
declaration
|
declaration
|
||||||
.append(";\n}\n\n");
|
.append(";\n}\n\n");
|
||||||
declaration.append('@')
|
// TODO: is it a problem that we never close the session?
|
||||||
.append(annotationMetaEntity.importType("jakarta.annotation.PreDestroy"))
|
if ( !isReactive() ) {
|
||||||
.append("\nprivate void closeSession() {")
|
declaration.append('@')
|
||||||
.append("\n\t")
|
.append(annotationMetaEntity.importType("jakarta.annotation.PreDestroy"))
|
||||||
.append(sessionVariableName)
|
.append("\nprivate void closeSession() {")
|
||||||
.append(".close();")
|
.append("\n\t")
|
||||||
.append("\n}\n\n");
|
.append(sessionVariableName)
|
||||||
|
.append(".close();")
|
||||||
|
.append("\n}\n\n");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
inject( declaration );
|
inject( declaration );
|
||||||
declaration
|
declaration
|
||||||
|
|
|
@ -89,11 +89,23 @@ public class IdFinderMethod extends AbstractFinderMethod {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void findWithNoFetchProfiles(StringBuilder declaration) {
|
private void findWithNoFetchProfiles(StringBuilder declaration) {
|
||||||
|
if ( isReactiveSession() ) {
|
||||||
|
declaration
|
||||||
|
.append(".chain(")
|
||||||
|
.append(localSessionName())
|
||||||
|
.append(" -> ")
|
||||||
|
.append(localSessionName());
|
||||||
|
}
|
||||||
declaration
|
declaration
|
||||||
.append(isUsingStatelessSession() ? ".get(" : ".find(")
|
.append(isUsingStatelessSession() ? ".get(" : ".find(")
|
||||||
.append(annotationMetaEntity.importType(entity))
|
.append(annotationMetaEntity.importType(entity))
|
||||||
.append(".class, ")
|
.append(".class, ")
|
||||||
.append(paramName)
|
.append(paramName);
|
||||||
|
if ( isReactiveSession() ) {
|
||||||
|
declaration
|
||||||
|
.append(')');
|
||||||
|
}
|
||||||
|
declaration
|
||||||
.append(");\n");
|
.append(");\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import static org.hibernate.processor.util.Constants.MUTINY_SESSION;
|
||||||
import static org.hibernate.processor.util.Constants.MUTINY_STATELESS_SESSION;
|
import static org.hibernate.processor.util.Constants.MUTINY_STATELESS_SESSION;
|
||||||
import static org.hibernate.processor.util.Constants.UNI;
|
import static org.hibernate.processor.util.Constants.UNI;
|
||||||
import static org.hibernate.processor.util.Constants.UNI_MUTINY_SESSION;
|
import static org.hibernate.processor.util.Constants.UNI_MUTINY_SESSION;
|
||||||
|
import static org.hibernate.processor.util.Constants.UNI_MUTINY_STATELESS_SESSION;
|
||||||
|
|
||||||
public class LifecycleMethod implements MetaAttribute {
|
public class LifecycleMethod implements MetaAttribute {
|
||||||
private final AnnotationMetaEntity annotationMetaEntity;
|
private final AnnotationMetaEntity annotationMetaEntity;
|
||||||
|
@ -110,7 +111,22 @@ public class LifecycleMethod implements MetaAttribute {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void delegateCall(StringBuilder declaration) {
|
private void delegateCall(StringBuilder declaration) {
|
||||||
if ( isReactive() ) {
|
if ( isReactiveSession() ) {
|
||||||
|
declaration
|
||||||
|
.append("\t\treturn ")
|
||||||
|
.append(sessionName)
|
||||||
|
.append(".chain(")
|
||||||
|
.append(localSessionName())
|
||||||
|
.append(" -> ")
|
||||||
|
.append(localSessionName())
|
||||||
|
.append('.')
|
||||||
|
.append(operationName)
|
||||||
|
.append('(')
|
||||||
|
.append(parameterName)
|
||||||
|
.append(')')
|
||||||
|
.append(')');
|
||||||
|
}
|
||||||
|
else if ( isReactive() ) {
|
||||||
declaration
|
declaration
|
||||||
.append("\t\treturn ")
|
.append("\t\treturn ")
|
||||||
.append(sessionName)
|
.append(sessionName)
|
||||||
|
@ -229,6 +245,16 @@ public class LifecycleMethod implements MetaAttribute {
|
||||||
private boolean isReactive() {
|
private boolean isReactive() {
|
||||||
return MUTINY_SESSION.equals(sessionType)
|
return MUTINY_SESSION.equals(sessionType)
|
||||||
|| MUTINY_STATELESS_SESSION.equals(sessionType)
|
|| MUTINY_STATELESS_SESSION.equals(sessionType)
|
||||||
|| UNI_MUTINY_SESSION.equals(sessionType);
|
|| UNI_MUTINY_SESSION.equals(sessionType)
|
||||||
|
|| UNI_MUTINY_STATELESS_SESSION.equals(sessionType);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isReactiveSession() {
|
||||||
|
return UNI_MUTINY_SESSION.equals(sessionType)
|
||||||
|
|| UNI_MUTINY_STATELESS_SESSION.equals(sessionType);
|
||||||
|
}
|
||||||
|
|
||||||
|
String localSessionName() {
|
||||||
|
return isReactiveSession() ? '_' + sessionName : sessionName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -106,7 +106,8 @@ public final class Constants {
|
||||||
public static final String TUPLE = "jakarta.persistence.Tuple";
|
public static final String TUPLE = "jakarta.persistence.Tuple";
|
||||||
|
|
||||||
public static final String UNI = "io.smallrye.mutiny.Uni";
|
public static final String UNI = "io.smallrye.mutiny.Uni";
|
||||||
public static final String UNI_MUTINY_SESSION = UNI+"<org.hibernate.reactive.mutiny.Mutiny.Session>";
|
public static final String UNI_MUTINY_SESSION = UNI + "<" + MUTINY_SESSION + ">";
|
||||||
|
public static final String UNI_MUTINY_STATELESS_SESSION = UNI + "<" + MUTINY_STATELESS_SESSION + ">";
|
||||||
public static final String UNI_INTEGER = UNI+"<java.lang.Integer>";
|
public static final String UNI_INTEGER = UNI+"<java.lang.Integer>";
|
||||||
public static final String UNI_VOID = UNI+"<java.lang.Void>";
|
public static final String UNI_VOID = UNI+"<java.lang.Void>";
|
||||||
public static final String UNI_BOOLEAN = UNI+"<java.lang.Boolean>";
|
public static final String UNI_BOOLEAN = UNI+"<java.lang.Boolean>";
|
||||||
|
|
Loading…
Reference in New Issue