Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- package ru.ifmo.rain.dorofeev.implementor;
- import info.kgeorgiy.java.advanced.implementor.ImplerException;
- import info.kgeorgiy.java.advanced.implementor.JarImpler;
- import javax.tools.JavaCompiler;
- import javax.tools.ToolProvider;
- import java.io.File;
- import java.io.IOException;
- import java.lang.reflect.*;
- import java.net.URISyntaxException;
- import java.nio.file.Files;
- import java.nio.file.Path;
- import java.nio.file.Paths;
- import java.security.CodeSource;
- import java.util.*;
- import java.util.jar.Attributes;
- import java.util.jar.JarOutputStream;
- import java.util.jar.Manifest;
- import java.util.stream.Collectors;
- import java.util.zip.ZipEntry;
- /**
- * @author Mark Dorofeev
- */
- public class JarImplementor implements JarImpler {
- public static class StringConstants {
- /**
- * File name extension for source file.
- */
- public static final String JAVA = "java";
- /**
- * Suffix of generated class name.
- */
- public static final String CLASS_NAME_SUFFIX = "Impl";
- /**
- * Empty string token.
- */
- public static final String EMPTY = "";
- /**
- * Space token.
- */
- public static final String SPACE = " ";
- /**
- * Tabulation token.
- */
- public static final String TAB = "\t";
- /**
- * New line token.
- */
- public static final String NEWLINE = System.lineSeparator();
- /**
- * Double new line token.
- */
- public static final String DOUBLE_NEWLINE = NEWLINE + NEWLINE;
- /**
- * Colon token.
- */
- public static final String COLON = ";";
- /**
- * Open curly bracket token.
- */
- public static final String CURLY_OPEN = "{";
- /**
- * Close curly bracket token.
- */
- public static final String CURLY_CLOSE = "}";
- /**
- * Open bracket token.
- */
- public static final String OPEN = "(";
- /**
- * Close bracket token.
- */
- public static final String CLOSE = ")";
- /**
- * Comma token.
- */
- public static final String COMMA = ",";
- /**
- * Null token.
- */
- public static final String NULL = "null";
- /**
- * True token.
- */
- public static final String TRUE = "true";
- /**
- * Zero token.
- */
- public static final String ZERO = "0";
- /**
- * String representation of keyword <code>package</code>
- */
- public static final String PACKAGE = "package ";
- /**
- * String representation of keyword <code>class</code>
- */
- public static final String CLASS = "class ";
- /**
- * String representation of keyword <code>implements</code>
- */
- public static final String IMPLEMENTS = "implements ";
- /**
- * String representation of keyword <code>extends</code>
- */
- public static final String EXTENDS = "extends ";
- /**
- * String representation of keyword <code>throws</code>
- */
- public static final String THROWS = "throws ";
- /**
- * String representation of keyword <code>public</code>
- */
- public static final String PUBLIC = "public ";
- /**
- * String representation of keyword <code>return</code>
- */
- public static final String RETURN = "return ";
- /**
- * String representation of keyword <code>super</code>
- */
- public static final String SUPER = "super";
- }
- /**
- * Stores implementation of generated class
- */
- private StringBuilder classImplementation;
- /**
- * Return package declaration with the following format:
- * <code>package a.b.c;</code>.
- *
- * @param token target type token
- * @return package declaration
- */
- private String getPackageDeclaration(Class<?> token) {
- if (token.getPackageName() != null) {
- return StringConstants.PACKAGE + token.getPackageName() + StringConstants.COLON + StringConstants.DOUBLE_NEWLINE;
- }
- return StringConstants.EMPTY;
- }
- /**
- * Return class declaration with the following format:
- * <code>class className;</code>
- *
- * @param token target type token
- * @return class declaration
- */
- private String getClassDeclaration(Class<?> token) {
- return StringConstants.PUBLIC + StringConstants.CLASS + token.getSimpleName() + StringConstants.CLASS_NAME_SUFFIX + StringConstants.SPACE
- + (token.isInterface() ? StringConstants.IMPLEMENTS : StringConstants.EXTENDS)
- + token.getCanonicalName()
- + StringConstants.SPACE + StringConstants.CURLY_OPEN + System.lineSeparator();
- }
- /**
- * Returns full path of the file with target class implementation.
- *
- * @param path initial path
- * @param token target type token
- * @param extension extension of target source file
- * @return full path to the file with target class implementation
- */
- private Path getFilePath(Path path, Class<?> token, String extension) {
- return path.resolve(token.getPackageName().replace('.', File.separatorChar)).resolve(token.getSimpleName() + StringConstants.CLASS_NAME_SUFFIX + '.' + extension);
- }
- /**
- * Returns full path of the file with target class implementation.
- *
- * @param token target type token
- * @return full path to the file with target class implementation
- */
- private String getFilePath1(Class<?> token) {
- return Paths.get("").resolve(token.getPackageName().replace('.', '/')).resolve(token.getSimpleName()).toString().replace('\\', '/');
- }
- /**
- * Creates a directory for output file if it doesn't exist.
- *
- * @param path target path
- * @throws ImplerException if an {@link IOException} has occurred
- */
- private void createDirectories(Path path) throws ImplerException {
- if (path.getParent() != null) {
- try {
- Files.createDirectories(path.getParent());
- } catch (IOException e) {
- throw new ImplerException(String.format("Unable to create directories for output file with path: %s", path.toString()), e);
- }
- }
- }
- /**
- * Compiles a provided source file using system java compiler.
- * This method uses class path used when launching the program so make sure you specified
- * all the paths (including modules) in the <code>-classpath</code> flag.
- *
- * @param token target type token
- * @param path target source file
- * @throws ImplerException if compilation error has occurred when compiling target source file.
- */
- private void compile(Class<?> token, Path path) throws ImplerException {
- String cp;
- try {
- CodeSource source = token.getProtectionDomain().getCodeSource();
- if (source == null) {
- cp = ".";
- } else {
- cp = Path.of(source.getLocation().toURI()).toString();
- }
- } catch (final URISyntaxException e) {
- throw new ImplerException("Cannot resolve classpath" + e.getMessage());
- }
- JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();
- String[] args = new String[]{"-encoding", "UTF-8",
- "-cp",
- path.toString() + File.pathSeparator +
- cp,
- Path.of(path.toString(), getFilePath1(token) + "Impl.java").toString()
- };
- if (javaCompiler == null || javaCompiler.run(null, null, null, args) != 0) {
- throw new ImplerException("Error during compiling generated java classes");
- }
- }
- /**
- * Creates a <code>.jar</code> file.
- * Note, that the obtained file is not executable and contains only one <code>.class</code> file.
- *
- * @param token target type token
- * @param path target path for the output <code>jar</code> file
- * @param sourcePath source file of <code>.class</code> file
- * @throws ImplerException if an internal {@link IOException} has occurred
- */
- private void createJar(Class<?> token, Path path, Path sourcePath) throws ImplerException {
- Manifest manifest = new Manifest();
- Attributes attributes = manifest.getMainAttributes();
- attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0");
- try (JarOutputStream stream = new JarOutputStream(Files.newOutputStream(path), manifest)) {
- String implementationPath = getFilePath1(token) + "Impl.class";
- stream.putNextEntry(new ZipEntry(implementationPath));
- Files.copy(Path.of(sourcePath.toString(), implementationPath), stream);
- } catch (IOException e) {
- throw new ImplerException("Writing a jar file error ", e);
- }
- }
- /**
- * Deletes a directory denoted by the path provided.
- *
- * @param path target path
- * @throws IOException if an internal {@link IOException} has occurred
- */
- private void deleteDirectory(Path path) throws IOException {
- Files.walk(path).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);
- }
- /**
- * Generates implementation of a class denoted by the provided type token and creates a <code>.jar</code>
- * file which contains that implementation in the provided path.
- *
- * @param token target type token
- * @param path target path
- * @throws ImplerException if:
- * <ul>
- * <li>One or more arguments are <code>null</code></li>
- * <li>Target class can't be extended</li>
- * <li>An internal {@link IOException} has occurred when handling I/O processes</li>
- * <li>There are no callable constructors in the target class</li>
- * </ul>
- */
- @Override
- public void implement(Class<?> token, Path path) throws ImplerException {
- path = getFilePath(path, token, StringConstants.JAVA);
- createDirectories(path);
- classImplementation = new StringBuilder();
- try (var writer = Files.newBufferedWriter(path)) {
- generateDeclaration(token);
- generateBody(token);
- generateEnd();
- writer.write(toUnicode(classImplementation.toString()));
- } catch (IOException ignored) {
- throw new ImplerException(String.format("Can't write implementation of a following class: %s", token.getCanonicalName()));
- }
- }
- /**
- * Generates implementation of a class denoted by the provided type token and creates a <code>.jar</code> file.
- *
- * @param token target type token
- * @param path target path
- * @throws ImplerException if:
- * <ul>
- * <li>One or more arguments are <code>null</code></li>
- * <li>Target class can't be extended</li>
- * <li>An internal {@link IOException} has occurred when handling I/O processes</li>
- * <li>{@link javax.tools.JavaCompiler} failed to compile target source file</li>
- * <li>There are no callable constructors in the target class</li>
- * </ul>
- */
- @Override
- public void implementJar(Class<?> token, Path path) throws ImplerException {
- Path sourcePath;
- createDirectories(path);
- try {
- sourcePath = Files.createTempDirectory(path.toAbsolutePath().getParent(), "temp");
- } catch (IOException e) {
- throw new ImplerException("Can't create a temporary directory.", e);
- }
- try {
- implement(token, sourcePath);
- compile(token, sourcePath);
- createJar(token, path, sourcePath);
- } finally {
- try {
- deleteDirectory(sourcePath);
- } catch (IOException e) {
- System.err.println("Can't delete the temporary directory.");
- }
- }
- }
- /**
- * Generates header of the generated class.
- * Header contains <code>package</code> declaration formed using {@link #getPackageDeclaration(Class)}
- * and <code>class</code> declaration formed using {@link #getClassDeclaration(Class)}
- *
- * @param token target type token
- */
- private void generateDeclaration(Class<?> token) {
- classImplementation.append(getPackageDeclaration(token));
- classImplementation.append(getClassDeclaration(token));
- }
- /**
- * Generates body of the generated class.
- * Body contains constructors' (only if the following class is not an interface) and abstract methods'
- * implementations.
- *
- * @param token target type token
- * @throws ImplerException if an internal {@link IOException} has occurred
- */
- private void generateBody(Class<?> token) throws ImplerException {
- if (!token.isInterface()) {
- generateConstructors(token);
- }
- generateAbstractMethods(token);
- }
- /**
- * Generates footer of the generated class.
- */
- private void generateEnd() {
- classImplementation.append(StringConstants.CURLY_CLOSE).append(StringConstants.DOUBLE_NEWLINE);
- }
- private String getReturnTypeAndName(Executable executable) {
- if (executable instanceof Method) {
- Method m = (Method) executable;
- return m.getReturnType().getCanonicalName() + StringConstants.SPACE + m.getName();
- } else {
- return executable.getDeclaringClass().getSimpleName() + StringConstants.CLASS_NAME_SUFFIX;
- }
- }
- /**
- * Generates arguments for {@link Executable}.
- * @param executable target executable
- * @param withType indicates if types should be provided
- * @return arguments for executable
- */
- private String getArguments(Executable executable, boolean withType) {
- StringBuilder sb = new StringBuilder();
- Parameter[] parameters = executable.getParameters();
- for (Parameter parameter : parameters) {
- sb.
- append(withType ? parameter.getType().getCanonicalName() : StringConstants.EMPTY)
- .append(StringConstants.SPACE).append(parameter.getName()).append(StringConstants.COMMA).append(StringConstants.SPACE);
- }
- return sb.length() > 0 ? sb.substring(0, sb.length() - 2) : sb.toString();
- }
- /**
- * Generates arguments for {@link Executable}.
- * @param executable target executable
- * @return arguments for executable
- */
- private String getArguments(Executable executable) {
- return getArguments(executable, true);
- }
- /**
- * Generates exceptions for {@link Executable}.
- * @param exceptions target exceptions
- * @return exceptions for executable
- */
- private String getExceptions(Class<?>[] exceptions) {
- StringBuilder stringBuilder = new StringBuilder();
- for (Class<?> exception : exceptions) {
- stringBuilder.append(exception.getCanonicalName()).append(StringConstants.COMMA + StringConstants.SPACE);
- }
- return stringBuilder.substring(0, stringBuilder.length() - 2);
- }
- /**
- * Generates return for {@link Executable}.
- * @param executable target executable
- * @return return for executable
- */
- private String getReturn(Executable executable) {
- if (executable instanceof Method) {
- Method m = (Method) executable;
- if (m.getReturnType() == void.class) {
- return StringConstants.EMPTY;
- } else if (m.getReturnType().isPrimitive()) {
- return m.getReturnType() == boolean.class ? StringConstants.TRUE : StringConstants.ZERO;
- }
- return StringConstants.NULL;
- }
- return StringConstants.EMPTY;
- }
- /**
- * Generates body for {@link Executable}.
- * @param executable target executable
- * @return body for executable
- */
- private String getBody(Executable executable) {
- return (executable instanceof Constructor ?
- StringConstants.TAB + StringConstants.TAB + StringConstants.SUPER + StringConstants.OPEN + getArguments(executable, false) + StringConstants.CLOSE + StringConstants.COLON + StringConstants.NEWLINE : StringConstants.EMPTY) +
- StringConstants.TAB + StringConstants.TAB + StringConstants.RETURN + StringConstants.SPACE + getReturn(executable) + StringConstants.COLON;
- }
- /**
- * Generates implementation of an {@link Executable}.
- * The executable must be either {@link Method} or {@link Constructor} to work correctly.
- * Implementation contains:
- * <ul>
- * <li>Access modifier</li>
- * <li>[optional] Type parameters (if it is a method)</li>
- * <li>[optional] Return type (if it is a method)</li>
- * <li>List of all checked exceptions thrown</li>
- * </ul>
- *
- * @param executable target executable
- */
- private void generateExecutable(Executable executable) {
- classImplementation.append(StringConstants.TAB).append(Modifier.toString(executable.getModifiers() & (Modifier.PUBLIC | Modifier.PROTECTED))).append(StringConstants.SPACE).append(getReturnTypeAndName(executable)).append(StringConstants.OPEN).append(getArguments(executable)).append(StringConstants.CLOSE).append(executable.getExceptionTypes().length == 0
- ? StringConstants.EMPTY
- : StringConstants.SPACE + StringConstants.THROWS + getExceptions(executable.getExceptionTypes()) + StringConstants.SPACE).append(StringConstants.CURLY_OPEN).append(StringConstants.NEWLINE).append(getBody(executable)).append(StringConstants.NEWLINE).append(StringConstants.TAB).append(StringConstants.CURLY_CLOSE).append(StringConstants.DOUBLE_NEWLINE);
- }
- /**
- * Add implementation of a constructor to the generated class.
- *
- * @param token target class
- */
- private void generateConstructors(Class<?> token) throws ImplerException {
- List<Constructor<?>> constructors = Arrays.stream(token.getDeclaredConstructors()).filter(c -> !Modifier.isPrivate(c.getModifiers())).collect(Collectors.toList());
- if (constructors.isEmpty()) {
- throw new ImplerException(String.format("Class %s has no callable constructors", token.getSimpleName()));
- }
- for (Constructor<?> constructor : constructors) {
- generateExecutable(constructor);
- }
- }
- /**
- * Generates all abstract methods of target type.
- *
- * @param token target type token
- */
- private void generateAbstractMethods(Class<?> token) {
- HashSet<CustomMethod> methods = new HashSet<>();
- fill(methods, token);
- methods.stream().filter(m -> !m.isOverridden).forEach(m -> generateExecutable(m.instance));
- }
- /**
- * A wrapper of {@link Method} for proper method comparing.
- */
- private class CustomMethod {
- /**
- * Instance of the method.
- */
- private Method instance;
- /**
- * A flag that shows if we need to override the method.
- * It is set to <code>true</code> if we met a <code>final</code> or just not <code>abstract</code> method in
- * the <code>super</code> class.
- * Otherwise, it is set to <code>false</code>.
- */
- private boolean isOverridden;
- /**
- * Constructor for the wrapper that receives instance and the flag.
- *
- * @param m target instance of {@link Method}
- * @param isOverridden target flag
- */
- CustomMethod(Method m, boolean isOverridden) {
- instance = m;
- this.isOverridden = isOverridden;
- }
- /**
- * Custom {@link Object#equals(Object)} method to compare two instances of {@link Method}.
- * Methods are equal if:
- * <ul>
- * <li>The other method is not <code>null</code></li>
- * <li>The other method's {@link #hashCode()} is the same</li>
- * </ul>
- *
- * @param obj target instance
- * @return <code>true</code> if instances are equal, <code>false</code> otherwise
- */
- @Override
- public boolean equals(Object obj) {
- if (obj == null) return false;
- if (obj instanceof CustomMethod) {
- return obj.hashCode() == hashCode();
- }
- return false;
- }
- /**
- * Calculates hash code of a {@link Method} instance
- * Formula: {@link Arrays#hashCode(Object[])} of type parameters plus {@link Class#hashCode()} of
- * return type plus {@link String#hashCode()} of name.
- *
- * @return hash code of the {@link #instance}
- */
- @Override
- public int hashCode() {
- return Arrays.hashCode(instance.getParameterTypes()) + instance.getReturnType().hashCode() + instance.getName().hashCode();
- }
- }
- /**
- * Adds all abstract methods of the target type to {@link Set} of {@link CustomMethod}
- *
- * @param methods target set
- * @param token target type token
- */
- private void fill(Set<CustomMethod> methods, Class<?> token) {
- if (token == null) return;
- methods.addAll(Arrays.stream(token.getDeclaredMethods()).map(m -> new CustomMethod(m, Modifier.isFinal(m.getModifiers()) || !Modifier.isAbstract(m.getModifiers()))).collect(Collectors.toSet()));
- Arrays.stream(token.getInterfaces()).forEach(i -> fill(methods, i));
- fill(methods, token.getSuperclass());
- }
- /**
- * Checks if a class can be extended.
- * Note: a class can't be extended if:
- * <ul>
- * <li>It is a primitive</li>
- * <li>It is final</li>
- * <li>It is array</li>
- * <li>It is enum</li>
- * <li>It is {@link Enum}</li>
- * </ul>
- *
- * @param token target type token
- * @throws ImplerException if the class can't be extended
- */
- private void validateClass(Class<?> token) throws ImplerException {
- if (token.isPrimitive() || token.isArray() || token.isEnum() || Modifier.isFinal(token.getModifiers()) || token == Enum.class || Modifier.isPrivate(token.getModifiers())) {
- throw new ImplerException(String.format("Invalid class with name: %s", token.getSimpleName()));
- }
- }
- /**
- * Return Unicode representation of the input string.
- *
- * @param input input string
- * @return Unicode representation of the input string
- */
- private String toUnicode(String input) {
- StringBuilder b = new StringBuilder();
- for (char c : input.toCharArray()) {
- if (c >= 128) {
- b.append(String.format("\\u%04X", (int) c));
- } else {
- b.append(c);
- }
- }
- return b.toString();
- }
- /**
- * Entry point of the program.
- * Usage: Implementor [-jar] <Class name> <Target path>
- *
- * @param args arguments
- */
- public static void main(String[] args) {
- if (args == null || args[0] == null || args[1] == null || (args.length > 2 && args[2] == null)) {
- System.out.println("Wrong arguments format");
- return;
- }
- for (String arg : args) {
- if (arg == null) {
- System.out.println("Null argument appeared");
- return;
- }
- }
- boolean isJar = "-jar".equals(args[0]);
- int offset = isJar ? 1 : 0;
- String classFullName = args[offset];
- String pathName = args[offset + 1];
- JarImplementor implementor = new JarImplementor();
- try {
- Class<?> token = Class.forName(classFullName);
- Path path = Paths.get(pathName);
- if (isJar) {
- implementor.implementJar(token, path);
- } else {
- implementor.implement(token, path);
- }
- } catch (ClassNotFoundException ignored) {
- System.err.println(String.format("Class with name %s not found", classFullName));
- } catch (ImplerException ignored) {
- System.err.println(String.format("Can't generate implementation of a class with full name: %s", classFullName));
- }
- }
- }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement