001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.rest.arg; 018 019import static org.apache.juneau.commons.utils.CollectionUtils.*; 020import static org.apache.juneau.commons.utils.StringUtils.*; 021import static org.apache.juneau.commons.utils.Utils.*; 022import static org.apache.juneau.http.annotation.QueryAnnotation.*; 023 024import java.util.*; 025 026import org.apache.juneau.*; 027import org.apache.juneau.annotation.*; 028import org.apache.juneau.collections.*; 029import org.apache.juneau.commons.reflect.*; 030import org.apache.juneau.http.annotation.*; 031import org.apache.juneau.httppart.*; 032import org.apache.juneau.rest.*; 033import org.apache.juneau.rest.annotation.*; 034import org.apache.juneau.rest.httppart.*; 035 036/** 037 * Resolves method parameters and parameter types annotated with {@link Query} on {@link RestOp}-annotated Java methods. 038 * 039 * <p> 040 * The parameter value is resolved using: 041 * <p class='bjava'> 042 * <jv>opSession</jv> 043 * .{@link RestOpSession#getRequest() getRequest}() 044 * .{@link RestRequest#getQueryParams() getQueryParams}() 045 * .{@link RequestQueryParams#get(String) get}(<jv>name</jv>) 046 * .{@link RequestQueryParam#as(Class) as}(<jv>type</jv>); 047 * </p> 048 * 049 * <p> 050 * {@link HttpPartSchema schema} is derived from the {@link Query} annotation. 051 * 052 * <p> 053 * If the {@link Schema#collectionFormat()} value is {@link HttpPartCollectionFormat#MULTI}, then the data type can be a {@link Collection} or array. 054 * 055 * <h5 class='section'>See Also:</h5><ul> 056 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JavaMethodParameters">Java Method Parameters</a> 057 * </ul> 058 */ 059public class QueryArg implements RestOpArg { 060 061 private static final AnnotationProvider AP = AnnotationProvider.INSTANCE; 062 063 /** 064 * Static creator. 065 * 066 * @param paramInfo The Java method parameter being resolved. 067 * @param annotations The annotations to apply to any new part parsers. 068 * @return A new {@link QueryArg}, or <jk>null</jk> if the parameter is not annotated with {@link Query}. 069 */ 070 public static QueryArg create(ParameterInfo paramInfo, AnnotationWorkList annotations) { 071 if (AP.has(Query.class, paramInfo)) 072 return new QueryArg(paramInfo, annotations); 073 return null; 074 } 075 076 /** 077 * Gets the merged @Query annotation combining class-level and parameter-level values. 078 * 079 * @param pi The parameter info. 080 * @param paramName The query parameter name. 081 * @return Merged annotation, or null if no class-level defaults exist. 082 */ 083 private static Query getMergedQuery(ParameterInfo pi, String paramName) { 084 // Get the declaring class 085 var declaringClass = pi.getMethod().getDeclaringClass(); 086 if (declaringClass == null) 087 return null; 088 089 // Find @Rest annotation on the class 090 var restAnnotation = declaringClass.getAnnotations(Rest.class).findFirst().map(AnnotationInfo::inner).orElse(null); 091 if (restAnnotation == null) 092 return null; 093 094 // Find matching @Query from class-level queryParams array 095 var classLevelQuery = (Query)null; 096 for (var q : restAnnotation.queryParams()) { 097 var qName = firstNonEmpty(q.name(), q.value()); 098 if (paramName.equals(qName)) { 099 classLevelQuery = q; 100 break; 101 } 102 } 103 104 if (classLevelQuery == null) 105 return null; 106 107 // Get parameter-level @Query 108 var paramQuery = AP.find(Query.class, pi).stream().findFirst().map(AnnotationInfo::inner).orElse(null); 109 110 if (paramQuery == null) { 111 // No parameter-level @Query, use class-level as-is 112 return classLevelQuery; 113 } 114 115 // Merge the two annotations: parameter-level takes precedence 116 return mergeAnnotations(classLevelQuery, paramQuery); 117 } 118 119 /** 120 * Merges two @Query annotations, with param-level taking precedence over class-level. 121 * 122 * @param classLevel The class-level default. 123 * @param paramLevel The parameter-level override. 124 * @return Merged annotation. 125 */ 126 private static Query mergeAnnotations(Query classLevel, Query paramLevel) { 127 // @formatter:off 128 return QueryAnnotation.create() 129 .name(firstNonEmpty(paramLevel.name(), paramLevel.value(), classLevel.name(), classLevel.value())) 130 .value(firstNonEmpty(paramLevel.value(), paramLevel.name(), classLevel.value(), classLevel.name())) 131 .def(firstNonEmpty(paramLevel.def(), classLevel.def())) 132 .description(paramLevel.description().length > 0 ? paramLevel.description() : classLevel.description()) 133 .parser(paramLevel.parser() != HttpPartParser.Void.class ? paramLevel.parser() : classLevel.parser()) 134 .serializer(paramLevel.serializer() != HttpPartSerializer.Void.class ? paramLevel.serializer() : classLevel.serializer()) 135 .schema(mergeSchemas(classLevel.schema(), paramLevel.schema())) 136 .build(); 137 // @formatter:on 138 } 139 140 /** 141 * Merges two @Schema annotations, with param-level taking precedence. 142 * 143 * @param classLevel The class-level default. 144 * @param paramLevel The parameter-level override. 145 * @return Merged annotation. 146 */ 147 private static Schema mergeSchemas(Schema classLevel, Schema paramLevel) { 148 // If parameter has a non-default schema, use it; otherwise use class-level 149 if (! SchemaAnnotation.empty(paramLevel)) 150 return paramLevel; 151 return classLevel; 152 } 153 154 private final boolean multi; 155 private final HttpPartParser partParser; 156 private final HttpPartSchema schema; 157 private final String name, def; 158 private final ClassInfo type; 159 160 /** 161 * Constructor. 162 * 163 * @param pi The Java method parameter being resolved. 164 * @param annotations The annotations to apply to any new part parsers. 165 */ 166 protected QueryArg(ParameterInfo pi, AnnotationWorkList annotations) { 167 // Get the query name from the parameter 168 this.name = findName(pi).orElseThrow(() -> new ArgException(pi, "@Query used without name or value")); 169 170 // Check for class-level defaults and merge if found 171 var mergedQuery = getMergedQuery(pi, name); 172 173 // Use merged query annotation for all lookups 174 this.def = nn(mergedQuery) && ! mergedQuery.def().isEmpty() ? mergedQuery.def() : findDef(pi).orElse(null); 175 this.type = pi.getParameterType(); 176 this.schema = nn(mergedQuery) ? HttpPartSchema.create(mergedQuery) : HttpPartSchema.create(Query.class, pi); 177 var pp = schema.getParser(); 178 this.partParser = nn(pp) ? HttpPartParser.creator().type(pp).apply(annotations).create() : null; 179 this.multi = schema.getCollectionFormat() == HttpPartCollectionFormat.MULTI; 180 181 if (multi && ! type.isCollectionOrArray()) 182 throw new ArgException(pi, "Use of multipart flag on @Query parameter that is not an array or Collection"); 183 } 184 185 @SuppressWarnings({ "rawtypes", "unchecked" }) 186 @Override /* Overridden from RestOpArg */ 187 public Object resolve(RestOpSession opSession) throws Exception { 188 var req = opSession.getRequest(); 189 var ps = partParser == null ? req.getPartParserSession() : partParser.getPartSession(); 190 var rh = req.getQueryParams(); 191 var bs = req.getBeanSession(); 192 var cm = bs.getClassMeta(type.innerType()); 193 194 if (multi) { 195 var c = cm.isArray() ? list() : (Collection)(cm.canCreateNewInstance() ? cm.newInstance() : new JsonList()); 196 rh.getAll(name).stream().map(x -> x.parser(ps).schema(schema).as(cm.getElementType()).orElse(null)).forEach(x -> c.add(x)); 197 return cm.isArray() ? toArray(c, cm.getElementType().inner()) : c; 198 } 199 200 if (cm.isMapOrBean() && isOneOf(name, "*", "")) { 201 var m = new JsonMap(); 202 rh.forEach(e -> m.put(e.getName(), e.parser(ps).schema(schema == null ? null : schema.getProperty(e.getName())).as(cm.getValueType()).orElse(null))); 203 return req.getBeanSession().convertToType(m, cm); 204 } 205 206 return rh.getLast(name).parser(ps).schema(schema).def(def).as(type.innerType()).orElse(null); 207 } 208}