/*
* Copyright (C) 2015 Strand Life Sciences.
*
* 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.strandls.alchemy.rest.client.stubgenerator;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import javax.ws.rs.Path;
import lombok.Cleanup;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
import org.apache.tools.ant.AntClassLoader;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.reflections.Reflections;
import org.stringtemplate.v4.STGroupDir;
import com.google.common.base.Predicate;
import com.google.common.collect.Sets;
import com.strandls.alchemy.rest.client.RestInterfaceAnalyzer;
import com.sun.codemodel.writer.FileCodeWriter;
/**
* Ant {@link Task} that scan classpath for matching rest service
* implementations and generates the stub, proxy implementation sources and
* corresponding guice bindings.
*
* @author Ashish Shinde
*
*/
@Setter
public class RestProxyGenerator extends Task {
/**
* The suffix to be appended to the generated package. Can be
* <code>null</code>.
*/
private String classSuffix;
/**
* The destination package for generated classes. Can be <code>null</code>
* or blank string for using the default package.
*/
private String destinationPackage = "";
/**
* Comma separated regexes on canonical class names to exclude.
*/
private String excludes;
/**
* Comma separated regexes on canonical class names to include, .
*/
private String includes;
/**
* The output directory where generated stubs would be added.
*/
private File outputDir;
/**
* The service classes.
*/
@Getter(lazy = true, onMethod = @_({
@edu.umd.cs.findbugs.annotations.SuppressWarnings(value = {
"JLM_JSR166_UTILCONCURRENT_MONITORENTER",
"RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE" },
justification = "Findbugs warnings on lombok generated code not critical."),
@SuppressWarnings("unchecked") }))
private final Set<Class<?>> serviceClasses = findServiceClasses();
/*
* (non-Javadoc)
* @see org.apache.tools.ant.Task#execute()
*/
@Override
public void execute() {
// validate parameters
validate();
log("Starting client code generation", Project.MSG_ERR);
try {
// generate service stubs.
generateServiceStubs();
// generate proxy implementations.
generateProxyImplementations();
// generate guice module with stub to proxy bindings.
generateGuiceModule();
} catch (final IOException e) {
throw new BuildException(e);
}
log("Finished client code generation", Project.MSG_ERR);
}
/**
* @return the rest webservice classes to process.
* @throws MalformedURLException
*/
private Set<Class<?>> findServiceClasses() {
final List<Object> params = new ArrayList<Object>();
final ClassLoader loader = getClass().getClassLoader();
if (loader instanceof AntClassLoader) {
final String[] path =
((AntClassLoader) loader).getClasspath().split(
System.getProperty("path.separator"));
for (final String string : path) {
try {
params.add(new URL(string));
} catch (final MalformedURLException e) {
try {
params.add(new URL("file://" + string));
} catch (final MalformedURLException e1) {
throw new RuntimeException(e1);
}
}
}
}
final Reflections reflections = new Reflections(params.toArray(new Object[0]));
final Set<Class<?>> allRestServices = reflections.getTypesAnnotatedWith(Path.class);
log("Locating service classes", Project.MSG_ERR);
final Set<Class<?>> filtered = Sets.filter(allRestServices, new Predicate<Class<?>>() {
@Override
public boolean apply(final Class<?> input) {
return doesMatch(input, includes) && !doesMatch(input, excludes);
}
/**
* Indicates if the input class matches any one of the comma
* separated regex patterns.
*
* <code>null</code> or empty pattern implies no match.
*
* @param input
* the input class.
* @param patterns
* comma separated list of patterns.
*
* @return <code>true</code> if any one of the pattern matches the
* canonical name of the class.
*/
private boolean doesMatch(final Class<?> input, final String patterns) {
boolean matches = false;
if (patterns != null) {
for (final String include : patterns.split("\\s*,\\s*")) {
matches |= Pattern.matches(include, input.getCanonicalName());
}
}
return matches;
}
});
for (final Class<?> klass : filtered) {
log("Will process class: " + klass.getCanonicalName(), Project.MSG_ERR);
}
return filtered;
}
/**
* Generate the guice module with bindings.
*
* @throws IOException
*/
private void generateGuiceModule() throws IOException {
final Set<Class<?>> classes = getServiceClasses();
new STGroupDir(getClass().getPackage().getName().replaceAll("\\.", "/"));
final File generatedSourceDirectory = getTargetSourceDirectory();
final Map<String, String> stubProxyMap = new HashMap<>();
for (final Class<?> klass : classes) {
stubProxyMap.put(getStubClassName(klass), getProxyImplemenationName(klass));
}
log("Generating guice module", Project.MSG_ERR);
final GuiceModuleGenerator moduleGenerator = new GuiceModuleGenerator();
moduleGenerator.generateGuiceModule(destinationPackage, generatedSourceDirectory,
stubProxyMap);
}
/**
* Generate the proxy implementations for the services.
*
* @throws IOException
*/
private void generateProxyImplementations() throws IOException {
final Set<Class<?>> classes = getServiceClasses();
new STGroupDir(getClass().getPackage().getName().replaceAll("\\.", "/"));
final File generatedSourceDirectory = getTargetSourceDirectory();
log("Generating proxies", Project.MSG_ERR);
final ProxyImplementationGenerator proxyGenerator = new ProxyImplementationGenerator();
for (final Class<?> klass : classes) {
proxyGenerator.generateProxy(getStubClassName(klass), getProxyImplemenationName(klass),
destinationPackage, generatedSourceDirectory);
log("Generated " + getProxyImplemenationName(klass), Project.MSG_INFO);
}
}
/**
* Generate service stubs.
*
* @throws IOException
*/
private void generateServiceStubs() throws IOException {
final ServiceStubGenerator stubGenerator =
new ServiceStubGenerator(new RestInterfaceAnalyzer());
final File generatedSourceDirectory = outputDir;
final Set<Class<?>> classes = getServiceClasses();
@Cleanup
final FileCodeWriter codeWriter = new FileCodeWriter(generatedSourceDirectory);
log("Generating service stubs", Project.MSG_ERR);
for (final Class<?> klass : classes) {
final String stubClassName = getStubClassName(klass);
try {
stubGenerator.generateStubInterface(klass, stubClassName, destinationPackage,
codeWriter);
log("Generated " + stubClassName, Project.MSG_INFO);
} catch (final Exception e) {
log("Stub generation failed for " + klass.getCanonicalName(), Project.MSG_ERR);
throw new RuntimeException(e);
}
}
}
/**
* The name of the generated proxy implementation.
*
* @param klass
* the service class.
* @return the name of the proxy implemenation
*/
private String getProxyImplemenationName(final Class<?> klass) {
return getStubClassName(klass) + "Proxy";
}
/**
* Return the name of the generated stub class.
*
* @param klass
* the service class.
* @return the name of the generated stub class.
*/
private String getStubClassName(final Class<?> klass) {
return klass.getSimpleName() + (!StringUtils.isBlank(classSuffix) ? classSuffix : "");
}
/**
* @return the target generates source directory.
*/
private File getTargetSourceDirectory() {
final String packageFolder =
!StringUtils.isBlank(destinationPackage) ? destinationPackage.replaceAll("\\.",
File.separator) : "";
final File generatedSourceDirectory =
!StringUtils.isBlank(packageFolder) ? new File(outputDir, packageFolder)
: outputDir;
if (!generatedSourceDirectory.isDirectory() && !generatedSourceDirectory.mkdirs()) {
throw new RuntimeException("Could not create " + generatedSourceDirectory);
}
return generatedSourceDirectory;
}
/**
* Validate the arguments.
*/
private void validate() {
if (outputDir == null || (outputDir.exists() && !outputDir.isDirectory())) {
throw new IllegalArgumentException("Not a valid directory : " + outputDir);
}
if (!outputDir.isDirectory() && !outputDir.mkdirs()) {
throw new RuntimeException("Could not create " + outputDir);
}
}
}