/*
* Copyright © 2014-2016 Cask Data, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package co.cask.cdap.internal.specification;
import co.cask.cdap.api.annotation.Batch;
import co.cask.cdap.api.annotation.ProcessInput;
import co.cask.cdap.api.annotation.Tick;
import co.cask.cdap.api.flow.FlowletDefinition;
import co.cask.cdap.api.flow.flowlet.InputContext;
import co.cask.cdap.internal.guava.reflect.TypeToken;
import co.cask.cdap.internal.lang.MethodVisitor;
import co.cask.cdap.internal.lang.Reflections;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
*
*/
public final class ProcessMethodExtractor extends MethodVisitor {
private final Map<String, Set<Type>> inputTypes;
private final Set<FlowletMethod> seenMethods;
public ProcessMethodExtractor(Map<String, Set<Type>> inputTypes) {
this.inputTypes = inputTypes;
this.seenMethods = new HashSet<>();
}
@Override
public void visit(Object instance, Type inspectType, Type declareType, Method method) throws Exception {
if (!seenMethods.add(FlowletMethod.create(method, inspectType))) {
// The method is already seen. It can only happen if a children class override a parent class method and
// is visting the parent method, since the method visiting order is always from the leaf class walking
// up the class hierarchy.
return;
}
ProcessInput processInputAnnotation = method.getAnnotation(ProcessInput.class);
Tick tickAnnotation = method.getAnnotation(Tick.class);
TypeToken<?> inspectTypeToken = TypeToken.of(inspectType);
if (processInputAnnotation == null && tickAnnotation == null) {
return;
}
// Check for tick method
if (tickAnnotation != null) {
checkArgument(processInputAnnotation == null,
"Tick method %s.%s should not have ProcessInput.",
inspectTypeToken.getRawType().getName(), method);
checkArgument(method.getParameterTypes().length == 0,
"Tick method %s.%s cannot have parameters.",
inspectTypeToken.getRawType().getName(), method);
return;
}
Type[] methodParams = method.getGenericParameterTypes();
checkArgument(methodParams.length > 0 && methodParams.length <= 2,
"Parameter missing from process method %s.%s.",
inspectTypeToken.getRawType().getName(), method);
// If there is more than one parameter there can only be exactly two; the second one must be InputContext type
if (methodParams.length == 2) {
checkArgument(InputContext.class.equals(TypeToken.of(methodParams[1]).getRawType()),
"Second parameter must be InputContext type for process method %s.%s.",
inspectTypeToken.getRawType().getName(), method);
}
// Extract the Input type from the first parameter of the process method
Type inputType = getInputType(inspectTypeToken, method, inspectTypeToken.resolveType(methodParams[0]).getType());
checkArgument(Reflections.isResolved(inputType),
"Invalid type in %s.%s. Only Class or ParameterizedType are supported.",
inspectTypeToken.getRawType().getName(), method);
List<String> inputNames = new LinkedList<>();
if (processInputAnnotation.value().length == 0) {
inputNames.add(FlowletDefinition.ANY_INPUT);
} else {
Collections.addAll(inputNames, processInputAnnotation.value());
}
for (String inputName : inputNames) {
Set<Type> types = inputTypes.get(inputName);
if (types == null) {
types = new HashSet<>();
inputTypes.put(inputName, types);
}
checkArgument(types.add(inputType),
"Same type already defined for the same input name %s in process method %s.%s.",
inputName, inspectTypeToken.getRawType().getName(), method);
}
}
private Type getInputType(TypeToken<?> type, Method method, Type methodParam) {
// In batch mode, if the first parameter is an iterator then extract the type information from
// the iterator's type parameter
if (method.getAnnotation(Batch.class) != null) {
if (methodParam instanceof ParameterizedType) {
ParameterizedType pType = (ParameterizedType) methodParam;
if (pType.getRawType().equals(Iterator.class)) {
methodParam = pType.getActualTypeArguments()[0];
}
}
} else {
// Check to see if there is an method param which is a type of iterator.
// This check is needed because we don't support type projection with iterator.
if (methodParam instanceof ParameterizedType) {
ParameterizedType pType = (ParameterizedType) methodParam;
checkArgument(!pType.getRawType().equals(Iterator.class),
"Iterator type should only be used with Batch annotation for process method %s.%s",
type.getRawType().getName(), method.getName());
}
}
return methodParam;
}
private void checkArgument(boolean condition, String template, Object...args) {
if (!condition) {
throw new IllegalArgumentException(String.format(template, args));
}
}
}