HHH-16633 introduce query methods to JPA metamodel generator

This commit is contained in:
Gavin King 2023-06-16 21:02:39 +02:00
parent d3e15a7cc1
commit 698b245753
13 changed files with 482 additions and 27 deletions

View File

@ -0,0 +1,66 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.annotations;
import org.hibernate.Incubating;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.CLASS;
/**
* Identifies a method of an abstract class or interface
* as defining the signature of a method which is used to
* execute the given {@linkplain #value HQL query}, and is
* generated automatically by the Hibernate Metamodel
* Generator.
* <p>
* For example:
* <pre>
* public interface Books {
* &#64;Hql("from Book where isbn = :isbn")
* Book findBookByIsbn(String isbn);
*
* &#64;Hql("from Book where title like ?1 order by title offset ?3 fetch first ?2 rows only")
* List&lt;Book&gt; findBooksByTitleWithPagination(String title, int max, int start);
*
* &#64;Hql("from Book where title like ?1")
* TypedQuery&lt;Book&gt; findBooksByTitle(String title);
* }
* </pre>
* <p>
* The Metamodel Generator automatically creates an
* "implementation" of these methods in the static metamodel
* class {@code Books_}.
* <pre>
* Book book = Books_.findBookByIsbn(session, isbn);
* List&lt;Book&gt; books = Books_.findBooksByTitleWithPagination(session, pattern, 10, 0);
* </pre>
* <p>
* The return type of an annotated method must be:
* <ul>
* <li>an entity type,
* <li>{@link java.util.List},
* <li>{@link org.hibernate.query.Query},
* <li>{@link jakarta.persistence.Query}, or
* <li>{@link jakarta.persistence.TypedQuery}.
* </ul>
* <p>
* The method parameters must match the parameters of the
* HQL query, either by name or by position.
*
* @author Gavin King
* @since 6.3
*/
@Target(METHOD)
@Retention(CLASS)
@Incubating
public @interface Hql {
String value();
}

View File

@ -21,6 +21,8 @@ ext {
dependencies {
implementation jakartaLibs.jaxbApi
implementation jakartaLibs.jaxb
implementation libs.antlrRuntime
implementation project( ':hibernate-core' )
xjc jakartaLibs.xjc
xjc jakartaLibs.jaxb

View File

@ -116,7 +116,8 @@ public final class ClassWriter {
List<MetaAttribute> members = entity.getMembers();
for (MetaAttribute metaMember : members) {
if (metaMember.hasTypedAttribute()) {
pw.println(" " + metaMember.getAttributeDeclarationString());
metaMember.getAttributeDeclarationString().lines()
.forEach(line -> pw.println(" " + line));
}
}
pw.println();

View File

@ -39,6 +39,9 @@ import org.hibernate.jpamodelgen.xml.JpaDescriptorParser;
import org.checkerframework.checker.nullness.qual.Nullable;
import static org.hibernate.jpamodelgen.util.Constants.QUERY_METHOD;
import static org.hibernate.jpamodelgen.util.TypeUtils.containsAnnotation;
/**
* Main annotation processor.
*
@ -137,6 +140,16 @@ public class JPAMetaModelEntityProcessor extends AbstractProcessor {
context.logMessage( Diagnostic.Kind.OTHER, "Processing annotated class " + element.toString() );
handleRootElementAuxiliaryAnnotationMirrors( element );
}
else if ( element instanceof TypeElement ) {
for ( Element enclosedElement : element.getEnclosedElements() ) {
if ( containsAnnotation( enclosedElement, QUERY_METHOD ) ) {
AnnotationMetaEntity metaEntity =
AnnotationMetaEntity.create( (TypeElement) element, context, false );
context.addMetaAuxiliary( metaEntity.getQualifiedName(), metaEntity );
break;
}
}
}
}
createMetaModelClasses();
@ -228,7 +241,7 @@ public class JPAMetaModelEntityProcessor extends AbstractProcessor {
}
private boolean isJPAEntity(Element element) {
return TypeUtils.containsAnnotation(
return containsAnnotation(
element,
Constants.ENTITY,
Constants.MAPPED_SUPERCLASS,
@ -237,7 +250,7 @@ public class JPAMetaModelEntityProcessor extends AbstractProcessor {
}
private boolean hasAuxiliaryAnnotations(Element element) {
return TypeUtils.containsAnnotation(
return containsAnnotation(
element,
Constants.NAMED_QUERY,
Constants.NAMED_QUERIES,
@ -275,8 +288,8 @@ public class JPAMetaModelEntityProcessor extends AbstractProcessor {
boolean requiresLazyMemberInitialization = false;
AnnotationMetaEntity metaEntity;
if ( TypeUtils.containsAnnotation( element, Constants.EMBEDDABLE ) ||
TypeUtils.containsAnnotation( element, Constants.MAPPED_SUPERCLASS ) ) {
if ( containsAnnotation( element, Constants.EMBEDDABLE ) ||
containsAnnotation( element, Constants.MAPPED_SUPERCLASS ) ) {
requiresLazyMemberInitialization = true;
}

View File

@ -6,6 +6,7 @@
*/
package org.hibernate.jpamodelgen.annotation;
import org.hibernate.jpamodelgen.model.MetaAttribute;
import org.hibernate.jpamodelgen.model.Metamodel;
import org.hibernate.jpamodelgen.util.Constants;
import org.hibernate.jpamodelgen.util.TypeUtils;
@ -66,5 +67,5 @@ public abstract class AnnotationMeta implements Metamodel {
});
}
abstract void putMember(String name, NameMetaAttribute nameMetaAttribute);
abstract void putMember(String name, MetaAttribute nameMetaAttribute);
}

View File

@ -7,19 +7,36 @@
package org.hibernate.jpamodelgen.annotation;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.ExecutableType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.tools.Diagnostic;
import org.antlr.v4.runtime.ANTLRErrorListener;
import org.antlr.v4.runtime.BailErrorStrategy;
import org.antlr.v4.runtime.DefaultErrorStrategy;
import org.antlr.v4.runtime.Parser;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.Recognizer;
import org.antlr.v4.runtime.atn.ATNConfigSet;
import org.antlr.v4.runtime.atn.PredictionMode;
import org.antlr.v4.runtime.dfa.DFA;
import org.antlr.v4.runtime.misc.ParseCancellationException;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.hibernate.grammars.hql.HqlLexer;
import org.hibernate.grammars.hql.HqlParser;
import org.hibernate.jpamodelgen.Context;
import org.hibernate.jpamodelgen.ImportContextImpl;
import org.hibernate.jpamodelgen.model.ImportContext;
@ -30,6 +47,14 @@ import org.hibernate.jpamodelgen.util.AccessTypeInformation;
import org.hibernate.jpamodelgen.util.Constants;
import org.hibernate.jpamodelgen.util.NullnessUtil;
import org.hibernate.jpamodelgen.util.TypeUtils;
import org.hibernate.query.hql.internal.HqlParseTreeBuilder;
import static java.util.stream.Collectors.toList;
import static org.hibernate.jpamodelgen.util.TypeUtils.containsAnnotation;
import static org.hibernate.jpamodelgen.util.TypeUtils.determineAnnotationSpecifiedAccessType;
import static org.hibernate.jpamodelgen.util.TypeUtils.getAnnotationMirror;
import static org.hibernate.jpamodelgen.util.TypeUtils.getAnnotationValue;
import static org.hibernate.query.hql.internal.StandardHqlTranslator.prettifyAntlrError;
/**
* Class used to collect meta information about an annotated type (entity, embeddable or mapped superclass).
@ -37,6 +62,7 @@ import org.hibernate.jpamodelgen.util.TypeUtils;
* @author Max Andersen
* @author Hardy Ferentschik
* @author Emmanuel Bernard
* @author Gavin King
*/
public class AnnotationMetaEntity extends AnnotationMeta {
@ -86,6 +112,7 @@ public class AnnotationMetaEntity extends AnnotationMeta {
return entityAccessTypeInfo;
}
@Override
public final Context getContext() {
return context;
}
@ -168,7 +195,7 @@ public class AnnotationMetaEntity extends AnnotationMeta {
}
@Override
void putMember(String name, NameMetaAttribute nameMetaAttribute) {
void putMember(String name, MetaAttribute nameMetaAttribute) {
members.put( name, nameMetaAttribute );
}
@ -194,20 +221,26 @@ public class AnnotationMetaEntity extends AnnotationMeta {
List<? extends Element> methodsOfClass = ElementFilter.methodsIn( element.getEnclosedElements() );
List<Element> gettersAndSettersOfClass = new ArrayList<>();
List<ExecutableElement> queryMethods = new ArrayList<>();
for ( Element rawMethodOfClass: methodsOfClass ) {
if ( isGetterOrSetter( rawMethodOfClass ) ) {
gettersAndSettersOfClass.add( rawMethodOfClass );
}
else if ( rawMethodOfClass instanceof ExecutableElement
&& containsAnnotation( rawMethodOfClass, Constants.QUERY_METHOD ) ) {
queryMethods.add( (ExecutableElement) rawMethodOfClass );
}
}
addPersistentMembers( gettersAndSettersOfClass, AccessType.PROPERTY );
addAuxiliaryMembers();
addQueryMethods( queryMethods );
initialized = true;
}
/**
* Check if method respects Java Bean conventions for getter and setters.
*
@ -221,31 +254,30 @@ public class AnnotationMetaEntity extends AnnotationMeta {
List<? extends TypeMirror> methodParameterTypes = methodType.getParameterTypes();
TypeMirror returnType = methodType.getReturnType();
if(
methodSimpleName.startsWith("set") &&
methodParameterTypes.size() == 1 &&
"void".equalsIgnoreCase( returnType.toString() ) ) {
return true;
}
else if(
( methodSimpleName.startsWith("get") || methodSimpleName.startsWith("is") ) &&
methodParameterTypes.isEmpty() &&
!"void".equalsIgnoreCase( returnType.toString() ) ) {
return true;
}
else {
return false;
}
return isSetter(methodSimpleName, methodParameterTypes, returnType)
|| isGetter(methodSimpleName, methodParameterTypes, returnType);
}
private static boolean isGetter(String methodSimpleName, List<? extends TypeMirror> methodParameterTypes, TypeMirror returnType) {
return (methodSimpleName.startsWith("get") || methodSimpleName.startsWith("is"))
&& methodParameterTypes.isEmpty()
&& !"void".equalsIgnoreCase(returnType.toString());
}
private static boolean isSetter(String methodSimpleName, List<? extends TypeMirror> methodParameterTypes, TypeMirror returnType) {
return methodSimpleName.startsWith("set")
&& methodParameterTypes.size() == 1
&& "void".equalsIgnoreCase(returnType.toString());
}
private void addPersistentMembers(List<? extends Element> membersOfClass, AccessType membersKind) {
for ( Element memberOfClass : membersOfClass ) {
AccessType forcedAccessType = TypeUtils.determineAnnotationSpecifiedAccessType( memberOfClass );
AccessType forcedAccessType = determineAnnotationSpecifiedAccessType( memberOfClass );
if ( entityAccessTypeInfo.getAccessType() != membersKind && forcedAccessType == null ) {
continue;
}
if ( TypeUtils.containsAnnotation( memberOfClass, Constants.TRANSIENT )
if ( containsAnnotation( memberOfClass, Constants.TRANSIENT )
|| memberOfClass.getModifiers().contains( Modifier.TRANSIENT )
|| memberOfClass.getModifiers().contains( Modifier.STATIC ) ) {
continue;
@ -259,4 +291,159 @@ public class AnnotationMetaEntity extends AnnotationMeta {
}
}
private void addQueryMethods(List<ExecutableElement> queryMethods) {
for ( ExecutableElement method : queryMethods) {
addQueryMethod(method);
}
}
private void addQueryMethod(ExecutableElement method) {
final String methodName = method.getSimpleName().toString();
final TypeMirror returnType = method.getReturnType();
if ( returnType instanceof DeclaredType ) {
final DeclaredType declaredType = (DeclaredType) returnType;
final List<? extends TypeMirror> typeArguments = declaredType.getTypeArguments();
if ( typeArguments.size() == 0 ) {
if ( containsAnnotation( declaredType.asElement(), Constants.ENTITY ) ) {
addQueryMethod(method, methodName, declaredType.toString(), null);
}
else {
final String containerTypeName = declaredType.toString();
if (isLegalRawResultType(containerTypeName)) {
addQueryMethod(method, methodName, null, containerTypeName);
}
else {
displayError(method, "incorrect return type '" + containerTypeName + "'");
}
}
}
else if ( typeArguments.size() == 1 ) {
final String containerTypeName = declaredType.asElement().toString();
final String returnTypeName = typeArguments.get(0).toString();
if (isLegalGenericResultType(containerTypeName)) {
addQueryMethod(method, methodName, returnTypeName, containerTypeName);
}
else {
displayError(method, "incorrect return type '" + containerTypeName + "'");
}
}
else {
displayError(method, "incorrect return type '" + declaredType + "'");
}
}
}
private static boolean isLegalRawResultType(String containerTypeName) {
return containerTypeName.equals("java.util.List")
|| containerTypeName.equals("jakarta.persistence.Query")
|| containerTypeName.equals("org.hibernate.query.Query");
}
private static boolean isLegalGenericResultType(String containerTypeName) {
return containerTypeName.equals("java.util.List")
|| containerTypeName.equals("jakarta.persistence.TypedQuery")
|| containerTypeName.equals("org.hibernate.query.Query");
}
private void addQueryMethod(
ExecutableElement method,
String methodName,
@Nullable String returnTypeName, @Nullable String containerTypeName) {
final AnnotationMirror mirror = getAnnotationMirror(method, Constants.QUERY_METHOD );
if ( mirror != null ) {
final Object queryString = getAnnotationValue( mirror, "value" );
if ( queryString instanceof String ) {
final List<String> paramNames =
method.getParameters().stream()
.map(param -> param.getSimpleName().toString())
.collect(toList());
final List<String> paramTypes =
method.getParameters().stream()
.map(param -> param.asType().toString())
.collect(toList());
final String hql = (String) queryString;
final QueryMethod attribute =
new QueryMethod(
this,
methodName,
hql,
returnTypeName,
containerTypeName,
paramNames,
paramTypes
);
putMember( attribute.getPropertyName(), attribute );
checkParameters(method, paramNames, mirror, hql);
checkHqlSyntax(method, mirror, hql);
}
}
}
private void checkParameters(ExecutableElement method, List<String> paramNames, AnnotationMirror mirror, String hql) {
for (int i = 1; i <= paramNames.size(); i++) {
final String param = paramNames.get(i-1);
if ( !hql.contains(":" + param) && !hql.contains("?" + i) ) {
displayError(method, mirror, "missing query parameter for '" + param
+ "' (no parameter named :" + param + " or ?" + i + ")");
}
}
}
private void checkHqlSyntax(ExecutableElement method, AnnotationMirror mirror, String queryString) {
ANTLRErrorListener errorListener = new ANTLRErrorListener() {
@Override
public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line, int charPositionInLine, String message, RecognitionException e) {
displayError(method, mirror, "illegal HQL syntax - "
+ prettifyAntlrError( offendingSymbol, line, charPositionInLine, message, e, queryString, false ));
}
@Override
public void reportAmbiguity(Parser recognizer, DFA dfa, int startIndex, int stopIndex, boolean exact, BitSet ambigAlts, ATNConfigSet configs) {
}
@Override
public void reportAttemptingFullContext(Parser recognizer, DFA dfa, int startIndex, int stopIndex, BitSet conflictingAlts, ATNConfigSet configs) {
}
@Override
public void reportContextSensitivity(Parser recognizer, DFA dfa, int startIndex, int stopIndex, int prediction, ATNConfigSet configs) {
}
};
final HqlLexer hqlLexer = HqlParseTreeBuilder.INSTANCE.buildHqlLexer( queryString );
final HqlParser hqlParser = HqlParseTreeBuilder.INSTANCE.buildHqlParser( queryString, hqlLexer );
hqlLexer.addErrorListener( errorListener );
hqlParser.getInterpreter().setPredictionMode( PredictionMode.SLL );
hqlParser.removeErrorListeners();
hqlParser.addErrorListener( errorListener );
hqlParser.setErrorHandler( new BailErrorStrategy() );
try {
hqlParser.statement();
}
catch ( ParseCancellationException e) {
// reset the input token stream and parser state
hqlLexer.reset();
hqlParser.reset();
// fall back to LL(k)-based parsing
hqlParser.getInterpreter().setPredictionMode( PredictionMode.LL );
hqlParser.setErrorHandler( new DefaultErrorStrategy() );
hqlParser.statement();
}
}
private void displayError(ExecutableElement method, String message) {
context.getProcessingEnvironment().getMessager()
.printMessage( Diagnostic.Kind.ERROR, message, method );
}
private void displayError(ExecutableElement method, AnnotationMirror mirror, String message) {
context.getProcessingEnvironment().getMessager()
.printMessage( Diagnostic.Kind.ERROR, message, method, mirror,
mirror.getElementValues().entrySet().stream()
.filter( entry -> entry.getKey().getSimpleName().toString().equals("value") )
.map(Map.Entry::getValue).findAny().orElseThrow() );
}
}

View File

@ -46,6 +46,7 @@ public class AnnotationMetaPackage extends AnnotationMeta {
return new AnnotationMetaPackage( element, context );
}
@Override
public final Context getContext() {
return context;
}
@ -121,7 +122,7 @@ public class AnnotationMetaPackage extends AnnotationMeta {
}
@Override
void putMember(String name, NameMetaAttribute nameMetaAttribute) {
void putMember(String name, MetaAttribute nameMetaAttribute) {
members.put( name, nameMetaAttribute );
}
}

View File

@ -0,0 +1,155 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.jpamodelgen.annotation;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.hibernate.jpamodelgen.model.MetaAttribute;
import org.hibernate.jpamodelgen.model.Metamodel;
import java.util.List;
import static org.hibernate.jpamodelgen.util.StringUtil.getUpperUnderscoreCaseFromLowerCamelCase;
/**
* @author Gavin King
*/
public class QueryMethod implements MetaAttribute {
private final Metamodel annotationMetaEntity;
private final String methodName;
private final String queryString;
private final @Nullable String returnTypeName;
private final @Nullable String containerTypeName;
private final List<String> paramNames;
private final List<String> paramTypes;
public QueryMethod(
Metamodel annotationMetaEntity,
String methodName,
String queryString,
@Nullable
String returnTypeName,
@Nullable
String containerTypeName,
List<String> paramNames,
List<String> paramTypes
) {
this.annotationMetaEntity = annotationMetaEntity;
this.methodName = methodName;
this.queryString = queryString;
this.returnTypeName = returnTypeName;
this.containerTypeName = containerTypeName;
this.paramNames = paramNames;
this.paramTypes = paramTypes;
}
@Override
public boolean hasTypedAttribute() {
return true;
}
@Override
public String getAttributeDeclarationString() {
StringBuilder declaration = new StringBuilder();
declaration.append("public static ");
if (containerTypeName != null) {
declaration
.append(annotationMetaEntity.importType(containerTypeName));
if (returnTypeName != null) {
declaration
.append("<")
.append(annotationMetaEntity.importType(returnTypeName))
.append(">");
}
}
else if (returnTypeName != null) {
declaration.append(annotationMetaEntity.importType(returnTypeName));
}
declaration
.append(" ")
.append(methodName)
.append("(")
.append(annotationMetaEntity.importType("jakarta.persistence.EntityManager"))
.append(" entityManager");
for (int i =0; i<paramNames.size(); i++) {
declaration
.append(", ")
.append(annotationMetaEntity.importType(paramTypes.get(i)))
.append(" ")
.append(paramNames.get(i));
}
declaration
.append(")")
.append(" {")
.append("\n return entityManager.createQuery(")
.append(getUpperUnderscoreCaseFromLowerCamelCase(methodName));
if (returnTypeName != null) {
declaration
.append(", ")
.append(annotationMetaEntity.importType(returnTypeName))
.append(".class");
}
declaration.append(")");
for (int i = 1; i <= paramNames.size(); i++) {
String param = paramNames.get(i-1);
if (queryString.contains(":" + param)) {
declaration
.append("\n .setParameter(\"")
.append(param)
.append("\", ")
.append(param)
.append(")");
}
else if (queryString.contains("?" + i)) {
declaration
.append("\n .setParameter(")
.append(i)
.append(", ")
.append(param)
.append(")");
}
}
if ( containerTypeName == null) {
declaration.append("\n .getSingleResult()");
}
else if ( containerTypeName.equals("java.util.List") ) {
declaration.append("\n .getResultList()");
}
declaration.append(";\n}");
return declaration.toString();
}
@Override
public String getAttributeNameDeclarationString() {
return new StringBuilder().append("public static final String ")
.append(getUpperUnderscoreCaseFromLowerCamelCase(methodName))
.append(" = \"")
.append(queryString)
.append("\";")
.toString();
}
@Override
public String getMetaType() {
throw new UnsupportedOperationException();
}
@Override
public String getPropertyName() {
return methodName;
}
@Override
public String getTypeDeclaration() {
return "jakarta.persistence.Query";
}
@Override
public Metamodel getHostingEntity() {
return annotationMetaEntity;
}
}

View File

@ -6,6 +6,8 @@
*/
package org.hibernate.jpamodelgen.model;
import org.hibernate.jpamodelgen.Context;
import java.util.List;
import javax.lang.model.element.Element;
@ -30,4 +32,6 @@ public interface Metamodel extends ImportContext {
Element getElement();
boolean isMetaComplete();
Context getContext();
}

View File

@ -50,6 +50,8 @@ public final class Constants {
public static final String HIB_FILTER_DEF = "org.hibernate.annotations.FilterDef";
public static final String HIB_FILTER_DEFS = "org.hibernate.annotations.FilterDefs";
public static final String QUERY_METHOD = "org.hibernate.annotations.Hql";
public static final Map<String, String> COLLECTIONS = allCollectionTypes();
private static Map<String, String> allCollectionTypes() {

View File

@ -627,4 +627,9 @@ public class XmlMetaEntity implements Metamodel {
return ElementKind.METHOD;
}
}
@Override
public Context getContext() {
return context;
}
}

View File

@ -19,9 +19,10 @@ import static org.hibernate.jpamodelgen.test.util.TestUtil.assertPresenceOfNameF
*/
public class AuxiliaryTest extends CompilationTest {
@Test
@WithClasses({ Book.class, Main.class })
@WithClasses({ Book.class, Main.class, Dao.class })
public void testGeneratedAnnotationNotGenerated() {
System.out.println( TestUtil.getMetaModelSourceAsString( Main.class ) );
System.out.println( TestUtil.getMetaModelSourceAsString( Dao.class ) );
assertMetamodelClassGeneratedFor( Book.class );
assertMetamodelClassGeneratedFor( Main.class );
assertPresenceOfNameFieldInMetamodelFor(

View File

@ -0,0 +1,17 @@
package org.hibernate.jpamodelgen.test.namedquery;
import jakarta.persistence.TypedQuery;
import org.hibernate.annotations.Hql;
import java.util.List;
public interface Dao {
@Hql("from Book where title like ?1")
TypedQuery<Book> findByTitle(String title);
@Hql("from Book where title like ?1 order by title fetch first ?2 rows only")
List<Book> findFirstNByTitle(String title, int N);
@Hql("from Book where isbn = :isbn")
Book findByIsbn(String isbn);
}