001/** 002 * Copyright (C) 2006-2024 Talend Inc. - www.talend.com 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.talend.sdk.component.runtime.visitor; 017 018import static java.util.stream.Collectors.toList; 019 020import java.lang.annotation.Annotation; 021import java.lang.reflect.Method; 022import java.lang.reflect.Parameter; 023import java.lang.reflect.ParameterizedType; 024import java.lang.reflect.Type; 025import java.math.BigDecimal; 026import java.util.Arrays; 027import java.util.Collection; 028import java.util.Date; 029import java.util.HashSet; 030import java.util.List; 031import java.util.Map; 032import java.util.Optional; 033import java.util.Set; 034import java.util.stream.Collectors; 035import java.util.stream.Stream; 036 037import org.talend.sdk.component.api.component.AfterVariables.AfterVariable; 038import org.talend.sdk.component.api.component.AfterVariables.AfterVariableContainer; 039import org.talend.sdk.component.api.input.Assessor; 040import org.talend.sdk.component.api.input.Emitter; 041import org.talend.sdk.component.api.input.PartitionMapper; 042import org.talend.sdk.component.api.input.PartitionSize; 043import org.talend.sdk.component.api.input.Producer; 044import org.talend.sdk.component.api.input.Split; 045import org.talend.sdk.component.api.processor.AfterGroup; 046import org.talend.sdk.component.api.processor.BeforeGroup; 047import org.talend.sdk.component.api.processor.ElementListener; 048import org.talend.sdk.component.api.processor.Output; 049import org.talend.sdk.component.api.processor.OutputEmitter; 050import org.talend.sdk.component.api.processor.Processor; 051import org.talend.sdk.component.api.standalone.DriverRunner; 052import org.talend.sdk.component.api.standalone.RunAtDriver; 053import org.talend.sdk.component.runtime.reflect.Parameters; 054 055public class ModelVisitor { 056 057 private static final Set<Class<?>> SUPPORTED_AFTER_VARIABLES_TYPES = new HashSet<>(Arrays 058 .asList(Boolean.class, Byte.class, byte[].class, Character.class, Date.class, Double.class, Float.class, 059 BigDecimal.class, Integer.class, Long.class, Object.class, Short.class, String.class, List.class)); 060 061 public void visit(final Class<?> type, final ModelListener listener, final boolean validate) { 062 if (getSupportedComponentTypes().noneMatch(type::isAnnotationPresent)) { // unlikely but just in case 063 return; 064 } 065 if (getSupportedComponentTypes().filter(type::isAnnotationPresent).count() != 1) { // > 1 actually 066 throw new IllegalArgumentException("You can't mix @Emitter, @PartitionMapper and @Processor on " + type); 067 } 068 069 if (type.isAnnotationPresent(PartitionMapper.class)) { 070 if (validate) { 071 validatePartitionMapper(type); 072 } 073 listener.onPartitionMapper(type, type.getAnnotation(PartitionMapper.class)); 074 } else if (type.isAnnotationPresent(Emitter.class)) { 075 if (validate) { 076 validateEmitter(type); 077 } 078 listener.onEmitter(type, type.getAnnotation(Emitter.class)); 079 } else if (type.isAnnotationPresent(Processor.class)) { 080 if (validate) { 081 validateProcessor(type); 082 } 083 listener.onProcessor(type, type.getAnnotation(Processor.class)); 084 } else if (type.isAnnotationPresent(DriverRunner.class)) { 085 if (validate) { 086 validateDriverRunner(type); 087 } 088 listener.onDriverRunner(type, type.getAnnotation(DriverRunner.class)); 089 } 090 } 091 092 private void validatePartitionMapper(final Class<?> type) { 093 final boolean infinite = type.getAnnotation(PartitionMapper.class).infinite(); 094 final long count = Stream 095 .of(type.getMethods()) 096 .filter(m -> getPartitionMapperMethods(infinite).anyMatch(m::isAnnotationPresent)) 097 .flatMap(m -> getPartitionMapperMethods(infinite).filter(m::isAnnotationPresent)) 098 .distinct() 099 .count(); 100 if (count != (infinite ? 2 : 3)) { 101 throw new IllegalArgumentException( 102 type + " partition mapper must have exactly one @Assessor (if not infinite), " 103 + "one @Split and one @Emitter methods"); 104 } 105 final boolean stoppable = type.getAnnotation(PartitionMapper.class).stoppable(); 106 if (!infinite && stoppable) { 107 throw new IllegalArgumentException(type + " partition mapper when not infinite cannot set stoppable"); 108 } 109 // 110 // now validate the 2 methods of the mapper 111 // 112 if (!infinite) { 113 Stream.of(type.getMethods()).filter(m -> m.isAnnotationPresent(Assessor.class)).forEach(m -> { 114 if (m.getParameterCount() > 0) { 115 throw new IllegalArgumentException(m + " must not have any parameter"); 116 } 117 }); 118 } 119 Stream.of(type.getMethods()).filter(m -> m.isAnnotationPresent(Split.class)).forEach(m -> { 120 // for now, we could inject it by default but to ensure we can inject more later 121 // we must do that validation 122 if (Stream 123 .of(m.getParameters()) 124 .anyMatch(p -> !p.isAnnotationPresent(PartitionSize.class) 125 || (p.getType() != long.class && p.getType() != int.class))) { 126 throw new IllegalArgumentException(m + " must not have any parameter without @PartitionSize"); 127 } 128 final Type splitReturnType = m.getGenericReturnType(); 129 if (!ParameterizedType.class.isInstance(splitReturnType)) { 130 throw new IllegalArgumentException(m + " must return a Collection<" + type.getName() + ">"); 131 } 132 133 final ParameterizedType splitPt = ParameterizedType.class.cast(splitReturnType); 134 if (!Class.class.isInstance(splitPt.getRawType()) 135 || !Collection.class.isAssignableFrom(Class.class.cast(splitPt.getRawType()))) { 136 throw new IllegalArgumentException(m + " must return a List of partition mapper, found: " + splitPt); 137 } 138 139 final Type arg = splitPt.getActualTypeArguments().length != 1 ? null : splitPt.getActualTypeArguments()[0]; 140 if (!Class.class.isInstance(arg) || !type.isAssignableFrom(Class.class.cast(arg))) { 141 throw new IllegalArgumentException( 142 m + " must return a Collection<" + type.getName() + "> but found: " + arg); 143 } 144 }); 145 Stream 146 .of(type.getMethods()) 147 .filter(m -> m.isAnnotationPresent(Emitter.class)) 148 .forEach(m -> { 149 // for now we don't support injection propagation since the mapper should 150 // already own all the config 151 if (m.getParameterCount() > 0) { 152 throw new IllegalArgumentException(m + " must not have any parameter"); 153 } 154 }); 155 156 validateAfterVariableAnnotationDeclaration(type); 157 validateAfterVariableContainer(type); 158 } 159 160 private void validateEmitter(final Class<?> input) { 161 final List<Method> producers = 162 Stream.of(input.getMethods()).filter(m -> m.isAnnotationPresent(Producer.class)).collect(toList()); 163 if (producers.size() != 1) { 164 throw new IllegalArgumentException(input + " must have a single @Producer method"); 165 } 166 167 if (producers.get(0).getParameterCount() > 0) { 168 throw new IllegalArgumentException(producers.get(0) + " must not have any parameter"); 169 } 170 171 validateAfterVariableAnnotationDeclaration(input); 172 validateAfterVariableContainer(input); 173 } 174 175 private void validateDriverRunner(final Class<?> standalone) { 176 final List<Method> driverRunners = Stream 177 .of(standalone.getMethods()) 178 .filter(m -> m.isAnnotationPresent(RunAtDriver.class)) 179 .collect(toList()); 180 if (driverRunners.size() != 1) { 181 throw new IllegalArgumentException(standalone + " must have a single @RunAtDriver method"); 182 } 183 184 if (driverRunners.get(0).getParameterCount() > 0) { 185 throw new IllegalArgumentException(driverRunners.get(0) + " must not have any parameter"); 186 } 187 188 validateAfterVariableAnnotationDeclaration(standalone); 189 validateAfterVariableContainer(standalone); 190 } 191 192 private void validateProcessor(final Class<?> input) { 193 final List<Method> afterGroups = 194 Stream.of(input.getMethods()).filter(m -> m.isAnnotationPresent(AfterGroup.class)).collect(toList()); 195 afterGroups.forEach(m -> { 196 final List<Parameter> invalidParams = Stream.of(m.getParameters()).peek(p -> { 197 if (p.isAnnotationPresent(Output.class) && !validOutputParam(p)) { 198 throw new IllegalArgumentException("@Output parameter must be of type OutputEmitter"); 199 } 200 }) 201 .filter(p -> !p.isAnnotationPresent(Output.class)) 202 .filter(p -> !Parameters.isGroupBuffer(p.getParameterizedType())) 203 .collect(toList()); 204 if (!invalidParams.isEmpty()) { 205 throw new IllegalArgumentException("Parameter of AfterGroup method need to be annotated with Output"); 206 } 207 }); 208 209 final List<Method> producers = Stream 210 .of(input.getMethods()) 211 .filter(m -> m.isAnnotationPresent(ElementListener.class)) 212 .collect(toList()); 213 if (producers.size() > 1) { 214 throw new IllegalArgumentException(input + " must have a single @ElementListener method"); 215 } 216 if (producers.isEmpty() && afterGroups 217 .stream() 218 .noneMatch(m -> Stream.of(m.getGenericParameterTypes()).anyMatch(Parameters::isGroupBuffer))) { 219 throw new IllegalArgumentException(input 220 + " must have a single @ElementListener method or pass records as a Collection<Record|JsonObject> to its @AfterGroup method"); 221 } 222 223 if (!producers.isEmpty() && Stream.of(producers.get(0).getParameters()).peek(p -> { 224 if (p.isAnnotationPresent(Output.class) && !validOutputParam(p)) { 225 throw new IllegalArgumentException("@Output parameter must be of type OutputEmitter"); 226 } 227 }).filter(p -> !p.isAnnotationPresent(Output.class)).count() < 1) { 228 throw new IllegalArgumentException(input + " doesn't have the input parameter on its producer method"); 229 } 230 231 Stream.of(input.getMethods()).filter(m -> m.isAnnotationPresent(BeforeGroup.class)).forEach(m -> { 232 if (m.getParameterCount() > 0) { 233 throw new IllegalArgumentException(m + " must not have any parameter"); 234 } 235 }); 236 237 validateAfterVariableAnnotationDeclaration(input); 238 validateAfterVariableContainer(input); 239 } 240 241 private boolean validOutputParam(final Parameter p) { 242 if (!ParameterizedType.class.isInstance(p.getParameterizedType())) { 243 return false; 244 } 245 final ParameterizedType pt = ParameterizedType.class.cast(p.getParameterizedType()); 246 return OutputEmitter.class == pt.getRawType(); 247 } 248 249 private Stream<Class<? extends Annotation>> getPartitionMapperMethods(final boolean infinite) { 250 return infinite ? Stream.of(Split.class, Emitter.class) : Stream.of(Assessor.class, Split.class, Emitter.class); 251 } 252 253 private Stream<Class<? extends Annotation>> getSupportedComponentTypes() { 254 return Stream.of(Emitter.class, PartitionMapper.class, Processor.class, DriverRunner.class); 255 } 256 257 private void validateAfterVariableContainer(final Class<?> type) { 258 // component can't have more than one after variable container 259 List<Method> markedMethods = Stream 260 .of(type.getMethods()) 261 .filter(m -> m.isAnnotationPresent(AfterVariableContainer.class)) 262 .collect(toList()); 263 if (markedMethods.size() > 1) { 264 String methods = markedMethods.stream().map(Method::toGenericString).collect(Collectors.joining(",")); 265 throw new IllegalArgumentException("The methods can't have more than 1 after variable container. " 266 + "Current marked methods: " + methods); 267 } 268 269 // check parameter list 270 Optional 271 .of(markedMethods 272 .stream() 273 .filter(m -> m.getParameterCount() != 0) 274 .map(Method::toGenericString) 275 .collect(Collectors.joining(","))) 276 .filter(str -> !str.isEmpty()) 277 .ifPresent(str -> { 278 throw new IllegalArgumentException( 279 "The method is annotated with " + AfterVariableContainer.class.getCanonicalName() + "'" 280 + str + "' should have parameters."); 281 }); 282 283 // check incorrect return type 284 Optional 285 .of(markedMethods 286 .stream() 287 .filter(m -> !isValidAfterVariableContainer(m.getGenericReturnType())) 288 .map(Method::toGenericString) 289 .collect(Collectors.joining(","))) 290 .filter(it -> !it.isEmpty()) 291 .ifPresent(methods -> { 292 throw new IllegalArgumentException( 293 "The method '" + methods + "' has wrong return type. It should be Map<String, Object>."); 294 }); 295 } 296 297 /** 298 * Right now the valid container object for after variables is Map. 299 * Where the key is String and value is Object 300 */ 301 private static boolean isValidAfterVariableContainer(final Type type) { 302 if (!(type instanceof ParameterizedType)) { 303 return false; 304 } 305 306 final ParameterizedType paramType = (ParameterizedType) type; 307 if (!(paramType.getRawType() instanceof Class) || paramType.getActualTypeArguments().length != 2) { 308 return false; 309 } 310 311 final Class<?> containerType = (Class<?>) paramType.getRawType(); 312 return Map.class.isAssignableFrom(containerType) && paramType.getActualTypeArguments()[0].equals(String.class) 313 && paramType.getActualTypeArguments()[1].equals(Object.class); 314 } 315 316 private static void validateAfterVariableAnnotationDeclaration(final Class<?> type) { 317 List<String> incorrectDeclarations = Stream 318 .of(type.getAnnotationsByType(AfterVariable.class)) 319 .filter(annotation -> !SUPPORTED_AFTER_VARIABLES_TYPES.contains(annotation.type())) 320 .map(annotation -> "The after variable with name '" + annotation.value() + "' has incorrect type: '" 321 + annotation.type() + "'") 322 .collect(toList()); 323 if (!incorrectDeclarations.isEmpty()) { 324 String message = incorrectDeclarations 325 .stream() 326 .collect(Collectors.joining(",", "The after variables declared incorrectly. ", "")); 327 throw new IllegalArgumentException(message); 328 } 329 } 330}