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