Есть ли способ узнать, какой параметр обрабатывается в обработчике Jersey @__Param fromString?

API, с которым я работаю, решил принять UUID как строки с кодировкой Base32 вместо стандартного шестнадцатеричного, разделенного штрихами формата, который UUID.fromString() ожидает. Это означает, что я не могу просто написать @QueryParam UUID myUuid в качестве параметра метода, поскольку преобразование завершилось с ошибкой.

Я работаю над этим, создавая пользовательский объект с другим конвертером fromString, который будет использоваться с аннотациями @QueryString и @FormParam. Я хотел бы иметь возможность получить доступ к контексту преобразования в методе fromString, чтобы я мог обеспечить лучшие сообщения об ошибках. Прямо сейчас, все, что я могу сделать, это следующее:

public static Base32UUID fromString(String uuidString) {
    final UUID uuid = UUIDUtils.fromBase32(uuidString, false);
    if (null == uuid) {
        throw new InvalidParametersException(ImmutableList.of("Invalid uuid: " + uuidString));
    }
    return new Base32UUID(uuid);
}

Я хотел бы узнать, какой параметр имеет недопустимый UUID, поэтому мои зарегистрированные исключения и возвращенные пользовательские ошибки кристально чисты. В идеале мой метод преобразования будет иметь дополнительный параметр для деталей, например:

public static Base32UUID fromString(
    String uuidString,
    String parameterName // New parameter?
) {
    final UUID uuid = UUIDUtils.fromBase32(uuidString, false);
    if (null == uuid) {
        throw new InvalidParametersException(ImmutableList.of("Invalid uuid: " + uuidString
            + " for parameter " + parameterName));
    }
    return new Base32UUID(uuid);
}

Но это нарушит условное соглашение, что Джерси найдет метод синтаксического анализа:

  1. Имейте статический метод с именем valueOf или fromString, который принимает один аргумент String (см., например, Integer.valueOf(String) и java.util.UUID.fromString(String));

Я также посмотрел на ParamConverterProvider, который также может быть зарегистрирован для обеспечения преобразования, но, похоже, он не добавляет достаточного контекста. Ближе всего он представляет собой массив аннотаций, но из того, что я могу сказать об аннотации, вы не можете отступать оттуда, чтобы определить, какая переменная или метод аннотация включена. Я нашел this и эти примеры, но они не 't эффективно использовать параметр Annotations[] или выставлять любой контекст преобразования, который я могу видеть.

Есть ли способ получить эту информацию? Или мне нужно отступить к явному конверсионному вызову в моем методе конечных точек?

Если это имеет значение, я использую Dropwizard 0.8.0, который использует Jersey 2.16 и Jetty 9.2.9.v20150224.

Ответ 1

Так что это можно сделать с помощью ParamConverter/ParamConverterProvider. Нам просто нужно ввести ResourceInfo. Оттуда мы можем получить ресурс Method и просто сделать некоторое отражение. Ниже приведен пример реализации, который я тестировал и работает по большей части.

import java.lang.reflect.Type;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.annotation.Annotation;

import java.util.Set;
import java.util.HashSet;
import java.util.Collections;

import javax.ws.rs.FormParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.ext.Provider;
import javax.ws.rs.container.ResourceInfo;
import javax.ws.rs.ext.ParamConverter;
import javax.ws.rs.ext.ParamConverterProvider;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.InternalServerErrorException;

@Provider
public class Base32UUIDParamConverter implements ParamConverterProvider {

    @Context
    private javax.inject.Provider<ResourceInfo> resourceInfo;

    private static final Set<Class<? extends Annotation>> ANNOTATIONS;

    static {
        Set<Class<? extends Annotation>> annots = new HashSet<>();
        annots.add(QueryParam.class);
        annots.add(FormParam.class);
        ANNOTATIONS = Collections.<Class<? extends Annotation>>unmodifiableSet(annots);
    }

    @Override
    public <T> ParamConverter<T> getConverter(Class<T> type, 
                                              Type type1,
                                              Annotation[] annots) {

        // Check if it is @FormParam or @QueryParam
        for (Annotation annotation : annots) {
            if (!ANNOTATIONS.contains(annotation.annotationType())) {
                return null;
            }
        }

        if (Base32UUID.class == type) {
            return new ParamConverter<T>() {

                @Override
                public T fromString(String value) {
                    try {
                        Method method = resourceInfo.get().getResourceMethod();

                        Parameter[] parameters = method.getParameters();
                        Parameter actualParam = null;

                        // Find the actual matching parameter from the method.
                        for (Parameter param : parameters) {
                            Annotation[] annotations = param.getAnnotations();
                            if (matchingAnnotationValues(annotations, annots)) {
                                actualParam = param;
                            }
                        }

                        // null warning, but assuming my logic is correct, 
                        // null shouldn't be possible. Maybe check anyway :-)
                        String paramName = actualParam.getName();
                        System.out.println("Param name : " + paramName);

                        Base32UUID uuid = new Base32UUID(value, paramName);
                        return type.cast(uuid);
                    } catch (Base32UUIDException ex) {
                        throw new BadRequestException(ex.getMessage());
                    } catch (Exception ex) {
                        throw new InternalServerErrorException(ex);
                    }
                }

                @Override
                public String toString(T t) {
                    return ((Base32UUID) t).value;
                }
            };
        }

        return null;
    }

    private boolean matchingAnnotationValues(Annotation[] annots1, 
                                             Annotation[] annots2) throws Exception {

        for (Class<? extends Annotation> annotType : ANNOTATIONS) {
            if (isMatch(annots1, annots2, annotType)) {
                return true;
            }
        }
        return false;
    }

    private <T extends Annotation> boolean isMatch(Annotation[] a1, 
                                                   Annotation[] a2, 
                                                   Class<T> aType) throws Exception {
        T p1 = getParamAnnotation(a1, aType);
        T p2 = getParamAnnotation(a2, aType);
        if (p1 != null && p2 != null) {
            String value1 = (String) p1.annotationType().getMethod("value").invoke(p1);
            String value2 = (String) p2.annotationType().getMethod("value").invoke(p2);
            if (value1.equals(value2)) {
                return true;
            }
        }

        return false;
    }

    private <T extends Annotation> T getParamAnnotation(Annotation[] annotations, 
                                                        Class<T> paramType) {
        T paramAnnotation = null;
        for (Annotation annotation : annotations) {
            if (annotation.annotationType() == paramType) {
                paramAnnotation = (T) annotation;
                break;
            }
        }
        return paramAnnotation;
    }
}

Некоторые примечания о реализации

  • Самая важная часть - это то, как вводится ResourceInfo. Поскольку это нужно получить в контексте области запроса, я ввел javax.inject.Provider, что позволяет нам лениво получить объект. Когда мы на самом деле делаем get(), оно будет в пределах области запроса.

    Осторожность в том, что он get() должен быть вызван внутри метода fromString ParamConverter. Метод getConverter ParamConverterProvider вызывается много раз во время загрузки приложения, поэтому мы не можем попробовать и вызвать get() в течение этого времени.

  • java.lang.reflect.Parameter класс, который я использовал, - это класс Java 8, поэтому, чтобы использовать эту реализацию, вам нужно работать на Java 8. Если вы не используете Java 8, этот пост может помочь в попытке получить имя параметра другим способом.

  • В связи с вышеупомянутой точкой аргумент компилятора -parameters должен применяться при компиляции, чтобы иметь возможность получить доступ к формальному имени параметра, как указано здесь. Я просто положил его в maven-cmpiler-plugin, как указано в ссылке.

    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>2.5.1</version>
        <inherited>true</inherited>
        <configuration>
            <compilerArgument>-parameters</compilerArgument>
            <testCompilerArgument>-parameters</testCompilerArgument>
            <source>1.8</source>
            <target>1.8</target>
        </configuration>
    </plugin>
    

    Если вы этого не сделаете, вызов Parameter.getName() приведет к тому, что argX, X будет индексом параметра.

  • Реализация допускает только @FormParam и @QueryParam.

  • Важно отметить (что я усвоил трудный путь), то есть все исключения, которые не обрабатываются в ParamConverter (только применим к @QueryParam в этом случае), приведет к 404 без объяснения проблемы. Поэтому вам нужно убедиться, что вы обрабатываете свое исключение, если хотите другое поведение.


UPDATE

В приведенной выше версии есть ошибка:

// Check if it is @FormParam or @QueryParam
for (Annotation annotation : annots) {
    if (!ANNOTATIONS.contains(annotation.annotationType())) {
        return null;
    }
}

Вышеупомянутое вызывается во время проверки модели, когда getConverter вызывается для каждого параметра. Приведенный выше код работает только в одной аннотации. Если есть другая аннотация, кроме @QueryParam или @FormParam, скажем @NotNull, она не сработает. Остальная часть кода в порядке. Он действительно работает в предположении, что будет больше одной аннотации.

Исправление для вышеуказанного кода будет чем-то вроде

boolean hasParamAnnotation = false;
for (Annotation annotation : annots) {
    if (ANNOTATIONS.contains(annotation.annotationType())) {
        hasParamAnnotation = true;
        break;
    }
}

if (!hasParamAnnotation) return null;

Ответ 2

Чтобы развернуть на peeskillets ответ выше, вы также можете решить проблему с dropwizard и трикотажными изделиями, встроенными в проверку bean. Итак, вместо того, чтобы выбрасывать исключение из метода factory, вы должны сделать это:

public class Base32UUID{
@NotNull
private final UUID uuid;
private Base32UUID(UUID uuid){ 
   this.uuid = uuid;
}
public static Base32UUID fromString(String uuidString) {
   final UUID uuid = UUIDUtils.fromBase32(uuidString, false);
   return new Base32UUID(uuid);
   }
}

В вашем методе reousource вы должны аннотировать параметр с помощью @Valid, этого должно быть достаточно, чтобы dropwizard возвращал описательное сообщение об ошибке, однако, если вы хотите настроить возвращаемое значение, создайте и зарегистрируйте экземпляр exceptionmapper, например так:

public class ValidationMapper implements ExceptionMapper<ConstraintViolationException>{

@Context
UriInfo uri;
@Context
private javax.inject.Provider<ResourceInfo> resourceInfo;
@Override
 public Response toResponse(ConstraintViolationException exception) {
   return Response.ok().build();
 }

}

И в вашем классе приложения:

environment.jersey().register(ValidationMapper.class);

Как вы можете видеть, все необходимые ресурсы peeskillet, введенные в его примере paramconverter, могут быть введены в блок отображения исключений. Подход валидации bean кажется мне немного более подходящим для меня + после его установки, он может использоваться для проверки почти любого ввода в любом месте вашего приложения, а не только для нуль-проверок, но регулярные выражения совпадают, электронные письма, диапазоны номеров и т. и убедитесь, что приложение всегда возвращает соответствующий и соответствующим образом отформатированный ответ. В соответствии с dropwizard docs проверка должна работать из коробки, но мне пришлось добавить валидацию dropwizard и jersey- bean -validation для моего pom, чтобы он работал:

<dependency>
    <groupId>io.dropwizard</groupId>
    <artifactId>dropwizard-validation</artifactId>
    <version>0.8.2</version>
</dependency>
<dependency>
    <groupId>org.glassfish.jersey.ext</groupId>
    <artifactId>jersey-bean-validation</artifactId>
    <version>2.19</version>
    <exclusions>
        <exclusion>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
        </exclusion>
    </exclusions>
</dependency>