/****************************************************************************** * Copyright (C) 2014 Yevgeny Krasik * * * * 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 com.github.ykrasik.jaci.reflection; import com.github.ykrasik.jaci.api.CommandOutput; import com.github.ykrasik.jaci.api.CommandPath; import com.github.ykrasik.jaci.command.CommandDef; import com.github.ykrasik.jaci.command.CommandOutputPromise; import com.github.ykrasik.jaci.path.ParsedPath; import com.github.ykrasik.jaci.reflection.method.ReflectionMethodProcessor; import com.github.ykrasik.jaci.util.exception.SneakyException; import com.github.ykrasik.jaci.util.opt.Opt; import java.util.*; /** * Processes a class and it's inner classes and creates {@link CommandDef}s from qualifying methods. * * @author Yevgeny Krasik */ public class ReflectionClassProcessor { /** Will be injected into all processed instances. */ private final CommandOutputPromise outputPromise; private final ReflectionMethodProcessor methodProcessor; public ReflectionClassProcessor() { this(new CommandOutputPromise()); } /** * Package-protected for testing. */ ReflectionClassProcessor(CommandOutputPromise outputPromise) { this(outputPromise, new ReflectionMethodProcessor(outputPromise)); } /** * Package-protected for testing. */ ReflectionClassProcessor(CommandOutputPromise outputPromise, ReflectionMethodProcessor methodProcessor) { this.outputPromise = Objects.requireNonNull(outputPromise, "outputPromise"); this.methodProcessor = Objects.requireNonNull(methodProcessor, "methodProcessor"); } /** * Process the object's class and all declared inner classes * and return all parsed commands with their paths. * * @param instance Object to process. * @return The {@link CommandDef}s that were extracted out of the object. * @throws RuntimeException If any error occurs. */ public Map<ParsedPath, List<CommandDef>> processObject(Object instance) { final ClassContext context = new ClassContext(); doProcess(instance, context); return context.commandPaths; } private void doProcess(Object instance, ClassContext initialContext) { final Class<?> clazz = instance.getClass(); // All method paths will be appended to the class's top level path. final ParsedPath topLevelPath = getTopLevelPath(clazz); final ClassContext context = initialContext.appendPath(topLevelPath); // Process the object / class we were called with. processClass(instance, clazz, context); // Process any inner classes this class declares. final Class<?>[] declaredClasses = ReflectionUtils.getDeclaredClasses(clazz); for (Class<?> innerClass : declaredClasses) { // Only process inner classes that have a single arg ctor that takes the outer class as a param. final ReflectionConstructor<?> constructor = getInnerClassConstructor(innerClass, clazz); if (constructor != null) { final Object innerInstance = constructor.newInstance(instance); doProcess(innerInstance, context); } } } private ReflectionConstructor<?> getInnerClassConstructor(Class<?> innerClass, Class<?> outerClass) { try { return ReflectionUtils.getDeclaredConstructor(innerClass, outerClass); } catch (Exception e) { return null; } } private void processClass(Object instance, Class<?> clazz, ClassContext context) { // Inject our outputPromise into the processed instance. // Any commands declared in the instance will reference this outputPromise, which will eventually // contain a concrete implementation of a CommandOutput. injectOutputPromise(instance, clazz); // Create commands from all qualifying methods. final ReflectionMethod[] methods = ReflectionUtils.getMethods(clazz); for (ReflectionMethod method : methods) { processMethod(context, instance, method); } } private void injectOutputPromise(Object instance, Class<?> clazz) { try { final ReflectionField[] fields = ReflectionUtils.getDeclaredFields(clazz); for (ReflectionField field : fields) { if (field.getType() == CommandOutput.class) { field.setAccessible(true); field.set(instance, outputPromise); // Only inject the first CommandOutput - class shouldn't have more then 1 anyway. break; } } } catch (Exception e) { throw SneakyException.sneakyThrow(e); } } private void processMethod(ClassContext context, Object instance, ReflectionMethod method) { // Commands can only be created from qualifying methods. final Opt<CommandDef> commandDef = methodProcessor.process(instance, method); if (!commandDef.isPresent()) { return; } final ParsedPath commandPath = getCommandPath(method); context.addCommandDef(commandPath, commandDef.get()); } private ParsedPath getTopLevelPath(Class<?> clazz) { return getPathFromAnnotation(ReflectionUtils.getAnnotation(clazz, CommandPath.class)); } private ParsedPath getCommandPath(ReflectionMethod method) { return getPathFromAnnotation(method.getAnnotation(CommandPath.class)); } private ParsedPath getPathFromAnnotation(CommandPath annotation) { if (annotation != null) { return ParsedPath.toDirectory(annotation.value()); } else { // Annotation isn't present, set the default path to 'root'. // Composing any path with 'root' has no effect. return ParsedPath.root(); } } /** * Auxiliary class for collecting {@link CommandDef}s. */ private static class ClassContext { private final ParsedPath topLevelPath; private final Map<ParsedPath, List<CommandDef>> commandPaths; public ClassContext() { this(ParsedPath.root(), new HashMap<ParsedPath, List<CommandDef>>()); } private ClassContext(ParsedPath topLevelPath, Map<ParsedPath, List<CommandDef>> commandPaths) { this.topLevelPath = topLevelPath; this.commandPaths = commandPaths; } /** * Create a new context in which new commands will be added to the given path appended * to this context's path. * * @param path Path to append to this context's path. * @return A context in which new commands will be added to the given path appended to this context's path. */ public ClassContext appendPath(ParsedPath path) { final ParsedPath appendedPath = topLevelPath.append(path); return new ClassContext(appendedPath, commandPaths); } /** * Add a {@link CommandDef} to the given {@link ParsedPath}. * * @param path Path to add the command to. * @param commandDef CommandDef to add. */ public void addCommandDef(ParsedPath path, CommandDef commandDef) { // Compose the top level path of the declaring class with the command path. final ParsedPath composedPath = topLevelPath.append(path); List<CommandDef> commands = commandPaths.get(composedPath); if (commands == null) { commands = new ArrayList<>(); commandPaths.put(composedPath, commands); } commands.add(commandDef); } } }