2015-12-30 24 views
5

È possibile consentire più chiavi@QueryParam per un singolo oggetto/variabile in Jersey?Più chiavi @QueryParam per un singolo valore in Jersey

enter image description here

Actual:

@POST 
public Something getThings(@QueryParam("customer-number") Integer n) { 
    ... 
} 

così, se aggiungo ?customer-number=3 dopo l'URL funziona.

Expected:

voglio ottenere il comportamento precedente se aggiungo uno dei seguenti valori:

  • ?customer-number=3
  • ?customerNumber=3
  • ?customerNo=3

Obs:

  1. Il QueryParam annotazione assomiglia:

    ... 
    public @interface QueryParam { 
        String value(); 
    } 
    

    così, non può accettare più valori di stringa (come @Produces).

  2. L'approccio seguito consente all'utente di utilizzare chiavi multiple con lo stesso significato, allo stesso tempo (e voglio avere una condizione "OR" tra di loro):

    @POST 
    public Something getThings(@QueryParam("customer-number") Integer n1, 
              @QueryParam("customerNumber") Integer n2, 
              @QueryParam("customerNo") Integer n3) { 
        ... 
    } 
    
  3. Qualcosa di simile a questo doesn 't lavoro:

    @POST 
    public Something getThings(@QueryParam("customer-number|customerNumber|customerNo") Integer n) { 
        ... 
    } 
    

Come posso fare questo?

Dettagli:

  • Jersey 2.22.1
  • Java 8

risposta

1

Forse il modo più semplice e più facile sarebbe quella di utilizzare un costume @BeanParam:

Prima di definire il fagiolo personalizzato fondere tutti i parametri di query come:

class MergedIntegerValue { 

    private final Integer value; 

    public MergedIntegerValue(
     @QueryParam("n1") Integer n1, 
     @QueryParam("n2") Integer n2, 
     @QueryParam("n3") Integer n3) { 
    this.value = n1 != null ? n1 
     : n2 != null ? n2 
     : n3 != null ? n3 
     : null; 
    // Throw an exception if value == null ? 
    } 

    public Integer getValue() { 
    return value; 
    } 
} 

e quindi utilizzalo con @BeanParam nel tuo metodo di risorsa:

public Something getThings(
    @BeanParam MergedIntegerValue n) { 

    // Use n.getValue() ... 
} 

Riferimento: https://jersey.java.net/documentation/latest/user-guide.html#d0e2403

1

Per essere onesti: questo non è il modo in cui i servizi web dovrebbero essere progettati. Si stabilisce un contratto rigoroso seguito sia dal cliente che dal server; tu definisci un parametro e basta.

Ma ovviamente sarebbe un mondo perfetto in cui si ha la libertà di dettare ciò che sta per accadere. Quindi se devi consentire tre parametri, allora dovrai farlo. Questo è un modo seguente approccio # 2, che devo fornire senza essere in grado di provarlo per goofs:

public Something getThings(@QueryParam("customer-number") Integer n1, 
          @QueryParam("customerNumber") Integer n2, 
          @QueryParam("customerNo") Integer n3) throws YourFailureException { 

    Integer customerNumber = getNonNullValue("Customer number", n1, n2, n3); 
    // things with stuff 
} 

private static Integer getNonNullValue(String label, Integer... params) throws YourFailureException { 

    Integer value = null; 

    for(Integer choice : params){ 
    if(choice != null){ 
     if(value != null){ 
     // this means there are at least two query parameters passed with a value 
     throw new YourFailureException("Ambiguous " + label + " parameters"); 
     } 

     value = choice; 
    } 
    } 

    if(value == null){ 
    throw new YourFailureException("Missing " + label + " parameter"); 
    } 

    return value; 
} 

Quindi, in pratica rifiutare qualsiasi chiamata che non passa specificamente uno dei parametri, e lasciare che un mapper un'eccezione tradurre l'eccezione si inserisce in un codice di risposta HTTP nella gamma 4xx, ovviamente.

(Ho fatto il metodo getNonNullValue() statico è mi sembra una funzione di utilità riutilizzabile).

1

È possibile creare un'annotazione personalizzata. Non entrerò troppo nel modo di farlo, puoi vedere this post o this post. Fondamentalmente si basa su un'infrastruttura diversa dalla solita iniezione di dipendenza con Jersey. Puoi vedere this package dal progetto Jersey. Qui è dove vivono tutti i fornitori di iniezione che gestiscono le iniezioni @XxxParam. Se esamini il codice sorgente, vedrai che le implementazioni sono abbastanza simili. I due collegamenti che ho fornito sopra seguono lo stesso schema, così come il codice qui sotto.

Quello che ho fatto è stato creato un un'annotazione personalizzata

@Target({ElementType.FIELD, ElementType.PARAMETER}) 
@Retention(RetentionPolicy.RUNTIME) 
public @interface VaryingParam { 

    String value(); 

    @SuppressWarnings("AnnotationAsSuperInterface") 
    public static class Factory 
      extends AnnotationLiteral<VaryingParam> implements VaryingParam { 

     private final String value; 

     public static VaryingParam create(final String newValue) { 
      return new Factory(newValue); 
     } 

     public Factory(String newValue) { 
      this.value = newValue; 
     } 

     @Override 
     public String value() { 
      return this.value; 
     } 
    } 
} 

Può sembrare strano che io abbia una fabbrica per creare, ma questo è stato richiesto per l'attuazione del codice qui sotto, in cui ho diviso il valore di la stringa e termina creando una nuova istanza di annotazione per ogni valore di divisione.

Ecco lo ValueFactoryProvider (che, se avete letto uno degli articoli precedenti, vedrete che è necessario per l'iniezione dei parametri del metodo personalizzato). È una classe grande, solo perché ho messo tutte le classi richieste in una singola classe, seguendo lo schema che vedi nel progetto Jersey.

public class VaryingParamValueFactoryProvider extends AbstractValueFactoryProvider { 

    @Inject 
    public VaryingParamValueFactoryProvider(
      final MultivaluedParameterExtractorProvider mpep, 
      final ServiceLocator locator) { 
     super(mpep, locator, Parameter.Source.UNKNOWN); 
    } 

    @Override 
    protected Factory<?> createValueFactory(final Parameter parameter) { 
     VaryingParam annotation = parameter.getAnnotation(VaryingParam.class); 
     if (annotation == null) { 
      return null; 
     } 
     String value = annotation.value(); 
     if (value == null || value.length() == 0) { 
      return null; 
     } 
     String[] variations = value.split("\\s*\\|\\s*"); 
     return new VaryingParamFactory(variations, parameter); 
    } 

    private static Parameter cloneParameter(final Parameter original, final String value) { 
     Annotation[] annotations = changeVaryingParam(original.getAnnotations(), value); 
     Parameter clone = Parameter.create(
       original.getRawType(), 
       original.getRawType(), 
       true, 
       original.getRawType(), 
       original.getRawType(), 
       annotations); 
     return clone; 
    } 

    private static Annotation[] changeVaryingParam(final Annotation[] annos, final String value) { 
     for (int i = 0; i < annos.length; i++) { 
      if (annos[i] instanceof VaryingParam) { 
       annos[i] = VaryingParam.Factory.create(value); 
       break; 
      } 
     } 
     return annos; 
    } 

    private class VaryingParamFactory extends AbstractContainerRequestValueFactory<Object> { 

     private final String[] variations; 
     private final Parameter parameter; 
     private final boolean decode; 
     private final Class<?> paramType; 
     private final boolean isList; 
     private final boolean isSet; 

     VaryingParamFactory(final String[] variations, final Parameter parameter) { 
      this.variations = variations; 
      this.parameter = parameter; 
      this.decode = !parameter.isEncoded(); 
      this.paramType = parameter.getRawType(); 
      this.isList = paramType == List.class; 
      this.isSet = paramType == Set.class; 
     } 

     @Override 
     public Object provide() { 
      MultivaluedParameterExtractor<?> e = null; 
      try { 
       Object value = null; 
       MultivaluedMap<String, String> params 
         = getContainerRequest().getUriInfo().getQueryParameters(decode); 
       for (String variant : variations) { 
        e = get(cloneParameter(parameter, variant)); 
        if (e == null) { 
         return null; 
        } 
        if (isList) { 
         List list = (List<?>) e.extract(params); 
         if (value == null) { 
          value = new ArrayList(); 
         } 
         ((List<?>) value).addAll(list); 
        } else if (isSet) { 
         Set set = (Set<?>) e.extract(params); 
         if (value == null) { 
          value = new HashSet(); 
         } 
         ((Set<?>) value).addAll(set); 
        } else { 
         value = e.extract(params); 
         if (value != null) { 
          return value; 
         }   
        } 
       } 
       return value; 
      } catch (ExtractorException ex) { 
       if (e == null) { 
        throw new ParamException.QueryParamException(ex.getCause(), 
          parameter.getSourceName(), parameter.getDefaultValue()); 
       } else { 
        throw new ParamException.QueryParamException(ex.getCause(), 
          e.getName(), e.getDefaultValueString()); 
       } 
      } 
     } 
    } 

    private static class Resolver extends ParamInjectionResolver<VaryingParam> { 

     public Resolver() { 
      super(VaryingParamValueFactoryProvider.class); 
     } 
    } 

    public static class Binder extends AbstractBinder { 

     @Override 
     protected void configure() { 
      bind(VaryingParamValueFactoryProvider.class) 
        .to(ValueFactoryProvider.class) 
        .in(Singleton.class); 
      bind(VaryingParamValueFactoryProvider.Resolver.class) 
        .to(new TypeLiteral<InjectionResolver<VaryingParam>>() { 
        }) 
        .in(Singleton.class); 
     } 
    } 
} 

Sarà necessario registrare questa classe Binder (parte inferiore della classe) con la maglia di usarlo.

Ciò che differenzia questa classe da Jersey QueryParamValueFactoryProvider è che invece di elaborare un solo valore String dell'annotazione, divide il valore e tenta di estrarre i valori dalla mappa param di query. Il primo valore trovato verrà restituito.Se il parametro è List o Set, continua a continuare a cercare tutte le opzioni e ad aggiungerle all'elenco.

Per la maggior parte questo mantiene tutte le funzionalità che ci si aspetta da un'annotazione @XxxParam. L'unica cosa che è stata difficile da implementare (quindi ho omesso di supportare questo caso d'uso), è costituita da più parametri, ad es.

@GET 
@Path("multiple") 
public String getMultipleVariants(@VaryingParam("param-1|param-2|param-3") String value1, 
            @VaryingParam("param-1|param-2|param-3") String value2) { 
    return value1 + ":" + value2; 
} 

Io in realtà non credo che dovrebbe essere difficile da attuare, se si ha realmente bisogno, è solo una questione di creazione di un nuovo MultivaluedMap, la rimozione di un valore se viene trovato. Questo sarebbe implementato nel metodo provide() dello VaryingParamFactory precedente. Se hai bisogno di questo caso d'uso, puoi semplicemente usare uno List o Set.

Vedere this GitHub Gist (è piuttosto lungo) per un test case completo, utilizzando Jersey Test Framework. Puoi vedere tutti i casi d'uso che ho provato nel QueryTestResource e dove registro lo Binder con il ResourceConfig nel metodo di prova configure().