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.server.arg; 018 019import static org.apache.juneau.commons.utils.CollectionUtils.*; 020import static org.apache.juneau.commons.utils.Shorts.*; 021import static org.apache.juneau.commons.utils.StringUtils.*; 022 023import java.util.*; 024 025import org.apache.juneau.commons.*; 026import org.apache.juneau.commons.httppart.*; 027import org.apache.juneau.commons.reflect.*; 028import org.apache.juneau.http.*; 029import org.apache.juneau.marshall.*; 030import org.apache.juneau.marshall.collections.*; 031import org.apache.juneau.marshall.httppart.*; 032import org.apache.juneau.rest.server.*; 033import org.apache.juneau.rest.server.httppart.*; 034 035/** 036 * Resolves method parameters and parameter types annotated with {@link Query} on {@link RestOp}-annotated Java methods. 037 * 038 * <p> 039 * The parameter value is resolved using: 040 * <p class='bjava'> 041 * <jv>opSession</jv> 042 * .{@link RestOpSession#getRequest() getRequest}() 043 * .{@link RestRequest#getQueryParams() getQueryParams}() 044 * .{@link RequestQueryParamList#get(String) get}(<jv>name</jv>) 045 * .{@link RequestQueryParam#as(Class) as}(<jv>type</jv>); 046 * </p> 047 * 048 * <p> 049 * {@link HttpPartSchema schema} is derived from the {@link Query} annotation. 050 * 051 * <p> 052 * If the {@link Schema#collectionFormat()} value is {@link HttpPartCollectionFormat#MULTI}, then the data type can be a {@link Collection} or array. 053 * 054 * <h5 class='section'>See Also:</h5><ul> 055 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JavaMethodParameters">Java Method Parameters</a> 056 * </ul> 057 */ 058@SuppressWarnings({ 059 "unchecked" // schema.getParser() is cast to Class<? extends HttpPartParser>, and resolve() adds to a raw Collection created for the parameter type 060}) 061public class QueryArg implements RestOpArg { 062 063 private static final AnnotationProvider AP = AnnotationProvider.INSTANCE; 064 065 /** 066 * Static creator. 067 * 068 * @param paramInfo The Java method parameter being resolved. Must not be <jk>null</jk>. 069 * @param annotations The annotations to apply to any new part parsers. 070 * @return A new {@link QueryArg}, or <jk>null</jk> if the parameter is not annotated with {@link Query}. 071 */ 072 public static QueryArg create(ParameterInfo paramInfo, AnnotationWorkList annotations) { 073 if (AP.has(Query.class, paramInfo)) 074 return new QueryArg(paramInfo, annotations); 075 return null; 076 } 077 078 private static Optional<String> findName(ParameterInfo pi) { 079 // @formatter:off 080 return AP.find(Query.class, pi) 081 .stream() 082 .map(AnnotationInfo::inner) 083 .filter(x -> isAnyNotBlank(x.value(), x.name())) 084 .findFirst() 085 .map(x -> firstNonBlank(x.name(), x.value())); 086 // @formatter:on 087 } 088 089 private static Optional<String> findDef(ParameterInfo pi) { 090 // @formatter:off 091 return AP.find(Query.class, pi) 092 .stream() 093 .map(AnnotationInfo::inner) 094 .filter(x -> ine(x.def())) 095 .findFirst() 096 .map(Query::def); 097 // @formatter:on 098 } 099 100 private static Query getClassLevelQuery(ParameterInfo pi, String paramName) { 101 var declaringClass = pi.getMethod().getDeclaringClass(); 102 if (declaringClass == null) 103 return null; 104 var restAnnotation = declaringClass.getAnnotations(Rest.class).findFirst().map(AnnotationInfo::inner).orElse(null); 105 if (restAnnotation == null) 106 return null; 107 for (var q : restAnnotation.queryParams()) { 108 var qName = firstNonEmpty(q.name(), q.value()); 109 if (eq(paramName, qName)) 110 return q; 111 } 112 return null; 113 } 114 115 private final boolean multi; 116 private final HttpPartParser partParser; 117 private final HttpPartSchema schema; 118 private final String name; 119 private final String def; 120 private final ClassInfo type; 121 122 /** 123 * Constructor. 124 * 125 * @param pi The Java method parameter being resolved. Must not be <jk>null</jk>. 126 * @param annotations The annotations to apply to any new part parsers. 127 */ 128 protected QueryArg(ParameterInfo pi, AnnotationWorkList annotations) { 129 this.name = findName(pi).orElseThrow(() -> new ArgException(pi, "@Query used without name or value")); 130 131 var classLevelQuery = getClassLevelQuery(pi, name); 132 133 var schemaBuilder = HttpPartSchema.create(); 134 if (classLevelQuery != null) 135 schemaBuilder.apply(classLevelQuery); 136 schemaBuilder.applyAll(Query.class, pi); 137 this.schema = schemaBuilder.build(); 138 139 this.def = findDef(pi).or(() -> o(classLevelQuery).filter(q -> ine(q.def())).map(Query::def)).orElse(null); 140 this.type = pi.getParameterType(); 141 var pp = (Class<? extends HttpPartParser>)schema.getParser(); 142 this.partParser = nn(pp) ? HttpPartParser.creator().type(pp).apply(annotations).create() : null; 143 this.multi = schema.getCollectionFormat() == HttpPartCollectionFormat.MULTI; 144 145 if (multi && ! type.isCollectionOrArray()) 146 throw new ArgException(pi, "Use of multipart flag on @Query parameter that is not an array or Collection"); 147 } 148 149 @SuppressWarnings({ 150 "rawtypes" // Raw types necessary for REST argument resolution with generic types 151 }) 152 @Override /* Overridden from RestOpArg */ 153 public Object resolve(RestOpSession opSession) throws Exception { 154 var req = opSession.getRequest(); 155 var ps = partParser == null ? req.getPartParserSession() : partParser.getPartSession(); 156 var rh = req.getQueryParams(); 157 var bs = req.getMarshallingSession(); 158 var cm = bs.getClassMeta(type.innerType()); 159 160 if (multi) { 161 Collection c; 162 if (cm.isArray()) { 163 c = list(); 164 } else if (cm.canCreateNewInstance()) { 165 c = (Collection)cm.newInstance(); 166 } else { 167 c = new JsonList(); 168 } 169 rh.getAll(name).stream().map(x -> x.parser(ps).schema(schema).as(cm.getElementType()).orElse(null)).forEach(c::add); 170 return cm.isArray() ? toArray(c, cm.getElementType().inner()) : c; 171 } 172 173 if (cm.isMapOrBean() && isOneOf(name, "*", "")) { 174 var m = new JsonMap(); 175 rh.forEach(e -> m.put(e.getName(), e.parser(ps).schema(schema == null ? null : schema.getProperty(e.getName())).as(cm.getValueType()).orElse(null))); 176 return req.getMarshallingSession().convertToType(m, cm); 177 } 178 179 return rh.getLast(name).parser(ps).schema(schema).def(def).as(type.innerType()).orElse(null); 180 } 181}