Skip to content

Improve error message if store module doesn't support a well-known fragment interface #2342

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
<version>2.5.0-SNAPSHOT</version>
<version>2.5.0-GH-2341-SNAPSHOT</version>

<name>Spring Data Core</name>

Expand Down Expand Up @@ -339,7 +339,7 @@
<version>0.1.4</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.jmolecules.integrations</groupId>
<artifactId>jmolecules-spring</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright 2021 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
*
* https://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.repository.core;

import org.springframework.dao.InvalidDataAccessApiUsageException;

/**
* Exception thrown in the context of repository creation.
*
* @author Mark Paluch
* @since 2.5
*/
@SuppressWarnings("serial")
public class RepositoryCreationException extends InvalidDataAccessApiUsageException {

private final Class<?> repositoryInterface;

/**
* Constructor for RepositoryCreationException.
*
* @param msg the detail message.
* @param repositoryInterface the repository interface.
*/
public RepositoryCreationException(String msg, Class<?> repositoryInterface) {
super(msg);
this.repositoryInterface = repositoryInterface;
}

/**
* Constructor for RepositoryException.
*
* @param msg the detail message.
* @param cause the root cause from the data access API in use.
* @param repositoryInterface the repository interface.
*/
public RepositoryCreationException(String msg, Throwable cause, Class<?> repositoryInterface) {
super(msg, cause);
this.repositoryInterface = repositoryInterface;
}

public Class<?> getRepositoryInterface() {
return repositoryInterface;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2021 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
*
* https://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.repository.core.support;

import org.springframework.data.repository.core.RepositoryCreationException;

/**
* Exception thrown during repository creation or repository method invocation when invoking a repository method on a
* fragment without an implementation.
*
* @author Mark Paluch
* @since 2.5
*/
@SuppressWarnings("serial")
public class FragmentNotImplementedException extends RepositoryCreationException {

private final RepositoryFragment<?> fragment;

/**
* Constructor for FragmentNotImplementedException.
*
* @param msg the detail message.
* @param repositoryInterface the repository interface.
* @param fragment the offending repository fragment.
*/
public FragmentNotImplementedException(String msg, Class<?> repositoryInterface, RepositoryFragment<?> fragment) {
super(msg, repositoryInterface);
this.fragment = fragment;
}

public RepositoryFragment<?> getFragment() {
return fragment;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2021 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
*
* https://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.repository.core.support;

import org.springframework.data.repository.core.RepositoryCreationException;

/**
* Exception thrown during repository creation when a the repository has custom methods that are not backed by a
* fragment or if no fragment could be found for a repository method invocation.
*
* @author Mark Paluch
* @since 2.5
*/
@SuppressWarnings("serial")
public class IncompleteRepositoryCompositionException extends RepositoryCreationException {

/**
* Constructor for IncompleteRepositoryCompositionException.
*
* @param msg the detail message.
* @param repositoryInterface the repository interface.
*/
public IncompleteRepositoryCompositionException(String msg, Class<?> repositoryInterface) {
super(msg, repositoryInterface);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.support.RepositoryInvocationMulticaster.DefaultRepositoryInvocationMulticaster;
import org.springframework.data.repository.core.support.RepositoryInvocationMulticaster.NoOpRepositoryInvocationMulticaster;
import org.springframework.data.repository.query.QueryCreationException;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.RepositoryQuery;
Expand Down Expand Up @@ -97,7 +98,13 @@ private Map<Method, RepositoryQuery> mapMethodsToQuery(RepositoryInformation rep

private Pair<Method, RepositoryQuery> lookupQuery(Method method, RepositoryInformation information,
QueryLookupStrategy strategy, ProjectionFactory projectionFactory) {
return Pair.of(method, strategy.resolveQuery(method, information, projectionFactory, namedQueries));
try {
return Pair.of(method, strategy.resolveQuery(method, information, projectionFactory, namedQueries));
} catch (QueryCreationException e) {
throw e;
} catch (RuntimeException e) {
throw QueryCreationException.create(e.getMessage(), e, information.getRepositoryInterface(), method);
}
}

@SuppressWarnings({ "rawtypes", "unchecked" })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -316,8 +316,12 @@ Method getMethod(Method method) {
public void validateImplementation() {

fragments.stream().forEach(it -> it.getImplementation() //
.orElseThrow(() -> new IllegalStateException(String.format("Fragment %s has no implementation.",
ClassUtils.getQualifiedName(it.getSignatureContributor())))));
.orElseThrow(() -> {
Class<?> repositoryInterface = metadata != null ? metadata.getRepositoryInterface() : Object.class;
return new FragmentNotImplementedException(String.format("Fragment %s used in %s has no implementation.",
ClassUtils.getQualifiedName(it.getSignatureContributor()),
ClassUtils.getQualifiedName(repositoryInterface)), repositoryInterface, it);
}));
}

/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
Expand All @@ -28,6 +29,7 @@
import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.interceptor.ExposeInvocationInterceptor;
import org.springframework.beans.BeanUtils;
Expand Down Expand Up @@ -57,12 +59,12 @@
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.util.ClassUtils;
import org.springframework.data.repository.util.QueryExecutionConverters;
import org.springframework.data.util.ReflectionUtils;
import org.springframework.lang.Nullable;
import org.springframework.transaction.interceptor.TransactionalProxy;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.ConcurrentReferenceHashMap.ReferenceType;
import org.springframework.util.ObjectUtils;
Expand Down Expand Up @@ -312,15 +314,16 @@ public <T> T getRepository(Class<T> repositoryInterface, RepositoryFragments fra

repositoryCompositionStep.end();

validate(information, composition);

StartupStep repositoryTargetStep = onEvent(applicationStartup, "spring.data.repository.target",
repositoryInterface);
Object target = getTargetRepository(information);

repositoryTargetStep.tag("target", target.getClass().getName());
repositoryTargetStep.end();

RepositoryComposition compositionToUse = composition.append(RepositoryFragment.implemented(target));
validate(information, compositionToUse);

// Create proxy
StartupStep repositoryProxyStep = onEvent(applicationStartup, "spring.data.repository.proxy", repositoryInterface);
ProxyFactory result = new ProxyFactory();
Expand Down Expand Up @@ -357,7 +360,6 @@ public <T> T getRepository(Class<T> repositoryInterface, RepositoryFragments fra
result.addAdvice(new QueryExecutorMethodInterceptor(information, projectionFactory, queryLookupStrategy,
namedQueries, queryPostProcessors, methodInvocationListeners));

RepositoryComposition compositionToUse = composition.append(RepositoryFragment.implemented(target));
result.addAdvice(
new ImplementationMethodExecutionInterceptor(information, compositionToUse, methodInvocationListeners));

Expand Down Expand Up @@ -502,17 +504,7 @@ protected Optional<QueryLookupStrategy> getQueryLookupStrategy(@Nullable Key key
*/
private void validate(RepositoryInformation repositoryInformation, RepositoryComposition composition) {

if (repositoryInformation.hasCustomMethod()) {

if (composition.isEmpty()) {

throw new IllegalArgumentException(
String.format("You have custom methods in %s but have not provided a custom implementation!",
repositoryInformation.getRepositoryInterface()));
}

composition.validateImplementation();
}
RepositoryValidator.validate(composition, getClass(), repositoryInformation);

validate(repositoryInformation);
}
Expand Down Expand Up @@ -606,7 +598,7 @@ public Object invoke(@SuppressWarnings("null") MethodInvocation invocation) thro
try {
return composition.invoke(invocationMulticaster, method, arguments);
} catch (Exception e) {
ClassUtils.unwrapReflectionException(e);
org.springframework.util.ReflectionUtils.handleReflectionException(e);
}

throw new IllegalStateException("Should not occur!");
Expand Down Expand Up @@ -715,4 +707,94 @@ public String toString() {
+ this.getRepositoryInterfaceName() + ", compositionHash=" + this.getCompositionHash() + ")";
}
}

/**
* Validator utility to catch common mismatches with a proper error message instead of letting the query mechanism
* attempt implementing a query method and fail with a less specific message.
*/
static class RepositoryValidator {

static Map<Class<?>, String> WELL_KNOWN_EXECUTORS = new HashMap<>();

static {

org.springframework.data.repository.util.ClassUtils.ifPresent(
"org.springframework.data.querydsl.QuerydslPredicateExecutor", RepositoryValidator.class.getClassLoader(),
it -> {
WELL_KNOWN_EXECUTORS.put(it, "Querydsl");
});

org.springframework.data.repository.util.ClassUtils.ifPresent(
"org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor",
RepositoryValidator.class.getClassLoader(), it -> {
WELL_KNOWN_EXECUTORS.put(it, "Reactive Querydsl");
});

org.springframework.data.repository.util.ClassUtils.ifPresent(
"org.springframework.data.repository.query.QueryByExampleExecutor",
RepositoryValidator.class.getClassLoader(), it -> {
WELL_KNOWN_EXECUTORS.put(it, "Query by Example");
});

org.springframework.data.repository.util.ClassUtils.ifPresent(
"org.springframework.data.repository.query.ReactiveQueryByExampleExecutor",
RepositoryValidator.class.getClassLoader(), it -> {
WELL_KNOWN_EXECUTORS.put(it, "Reactive Query by Example");
});
}

/**
* Validate the {@link RepositoryComposition} for custom implementations and well-known executors.
*
* @param composition
* @param source
* @param repositoryInformation
*/
public static void validate(RepositoryComposition composition, Class<?> source,
RepositoryInformation repositoryInformation) {

Class<?> repositoryInterface = repositoryInformation.getRepositoryInterface();
if (repositoryInformation.hasCustomMethod()) {

if (composition.isEmpty()) {

throw new IncompleteRepositoryCompositionException(
String.format("You have custom methods in %s but have not provided a custom implementation!",
org.springframework.util.ClassUtils.getQualifiedName(repositoryInterface)),
repositoryInterface);
}

composition.validateImplementation();
}

for (Map.Entry<Class<?>, String> entry : WELL_KNOWN_EXECUTORS.entrySet()) {

Class<?> executorInterface = entry.getKey();
if (!executorInterface.isAssignableFrom(repositoryInterface)) {
continue;
}

if (!containsFragmentImplementation(composition, executorInterface)) {
throw new UnsupportedFragmentException(
String.format("Repository %s implements %s but %s does not support %s!",
ClassUtils.getQualifiedName(repositoryInterface), ClassUtils.getQualifiedName(executorInterface),
ClassUtils.getShortName(source), entry.getValue()),
repositoryInterface, executorInterface);
}
}
}

private static boolean containsFragmentImplementation(RepositoryComposition composition,
Class<?> executorInterface) {

for (RepositoryFragment<?> fragment : composition.getFragments()) {

if (fragment.getImplementation().filter(executorInterface::isInstance).isPresent()) {
return true;
}
}

return false;
}
}
}
Loading