/* See LICENSE for licensing and NOTICE for copyright. */ package org.ldaptive.doc; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.net.URL; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; import java.nio.charset.StandardCharsets; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Scanner; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import com.sun.codemodel.JClassAlreadyExistsException; import com.sun.codemodel.JCodeModel; import com.sun.codemodel.JDefinedClass; import com.sun.codemodel.JDocComment; import com.sun.codemodel.JMethod; import com.sun.codemodel.JMod; /** * Utility class for creating Java POJOs from documentation source. * * @author Middleware Services */ public class ClassGenerator { /** Package to create classes in. */ private static final String PACKAGE_TO_CREATE = "org.ldaptive.doc"; /** Packages to import for compilation. */ private static final String[] PACKAGES_TO_IMPORT = new String[] { "java.io", "java.security.cert", "java.time", "java.util", "java.util.concurrent", "org.ldaptive", "org.ldaptive.ad", "org.ldaptive.ad.control", "org.ldaptive.ad.control.util", "org.ldaptive.ad.extended", "org.ldaptive.ad.handler", "org.ldaptive.async", "org.ldaptive.async.handler", "org.ldaptive.auth", "org.ldaptive.auth.ext", "org.ldaptive.beans", "org.ldaptive.beans.generate", "org.ldaptive.beans.persistence", "org.ldaptive.beans.reflect", "org.ldaptive.cache", "org.ldaptive.concurrent", "org.ldaptive.control", "org.ldaptive.control.util", "org.ldaptive.extended", "org.ldaptive.intermediate", "org.ldaptive.handler", "org.ldaptive.io", "org.ldaptive.pool", "org.ldaptive.props", "org.ldaptive.provider", "org.ldaptive.referral", "org.ldaptive.sasl", "org.ldaptive.schema", "org.ldaptive.ssl", "org.ldaptive.templates", }; /** String containing all import statements. */ private static final String IMPORT_STATEMENTS; /** Code model for java class creation. */ private final JCodeModel codeModel = new JCodeModel(); /** Sections to build beans for. */ private final Map<String, List<String>> sections = new HashMap<>(); /** Initialize {@link #IMPORT_STATEMENTS}. */ static { final StringBuilder sb = new StringBuilder(); for (String p : PACKAGES_TO_IMPORT) { sb.append("import ").append(p).append(".*;").append("\n"); } IMPORT_STATEMENTS = sb.toString(); } /** * Creates a new class generator. * * @param url to download zipped source from * @param path to write files to * * @throws IOException if the source cannot be downloaded, unzipped and read */ public ClassGenerator(final String url, final String path) throws IOException { // download the zipped source download(url, path + "/source.zip"); // unzip the source unzip(path + "/source.zip", path + "/doc-sources"); // read the source files final Path sourceDir = Paths.get(path + "/doc-sources"); Files.walkFileTree(sourceDir, new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException { if (attrs.isRegularFile()) { final String content = new Scanner(file.toFile()).useDelimiter("\\Z").next(); final String name = file.getName(file.getNameCount() - 2).toString(); final List<String> l; if (sections.containsKey(name)) { l = sections.get(name); } else { l = new ArrayList<>(); } l.add(content); sections.put(name, l); } return FileVisitResult.CONTINUE; } }); } /** * Download a file. * * @param url to download * @param destination to write the file at * * @throws IOException if an error occurs */ private static void download(final String url, final String destination) throws IOException { final URL download = new URL(url); final ReadableByteChannel rbc = Channels.newChannel(download.openStream()); final FileOutputStream fos = new FileOutputStream(destination); fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); fos.flush(); rbc.close(); } /** * Unzip a file. * * @param file to unzip * @param destination to expand the file at * * @throws IOException if an error occurs */ private static void unzip(final String file, final String destination) throws IOException { final File directory = new File(destination); // if the output directory doesn't exist, create it if (!directory.exists()) { directory.mkdirs(); } // buffer for read and write data to file final byte[] buffer = new byte[2048]; final ZipInputStream zipInput = new ZipInputStream(new FileInputStream(file)); ZipEntry entry = zipInput.getNextEntry(); try { while (entry != null) { final String entryName = entry.getName(); final File f = new File(destination + File.separator + entryName); // create the directories of the zip directory if (entry.isDirectory()) { final File newDir = new File(f.getAbsolutePath()); if (!newDir.exists()) { newDir.mkdirs(); } } else { try (FileOutputStream fOutput = new FileOutputStream(f)) { int count; while ((count = zipInput.read(buffer)) > 0) { // write 'count' bytes to the file output stream fOutput.write(buffer, 0, count); } } } // close ZipEntry and take the next one zipInput.closeEntry(); entry = zipInput.getNextEntry(); } } finally { // close the last ZipEntry zipInput.closeEntry(); zipInput.close(); } } /** * Generates a class for each doc section. */ public void generate() { for (Map.Entry<String, List<String>> entry : sections.entrySet()) { final JDefinedClass definedClass = createClass(PACKAGE_TO_CREATE, entry.getKey()); final JDocComment jDocComment = definedClass.javadoc(); jDocComment.add(String.format("Ldaptive generated bean for section '%s'", entry.getKey())); final List<String> names = entry.getValue(); for (int i = 0; i < names.size(); i++) { createMethod(definedClass, entry.getKey() + (i + 1), names.get(i)); } } } /** * Creates a class in the supplied package. * * @param classPackage to place the class in * @param className to create * * @return class * * @throws IllegalArgumentException if the class already exists */ protected JDefinedClass createClass(final String classPackage, final String className) { String fqClassName; if (!Character.isUpperCase(className.charAt(0))) { fqClassName = String.format( "%s.%s", classPackage, className.substring(0, 1).toUpperCase() + className.substring(1, className.length())); } else { fqClassName = String.format("%s.%s", classPackage, className); } try { return codeModel._class(fqClassName); } catch (JClassAlreadyExistsException e) { throw new IllegalArgumentException("Class already exists: " + fqClassName, e); } } /** * Creates a method on the supplied class with the supplied name. * * @param clazz to put getter and setter methods on * @param name of the property * @param body content of the method */ protected void createMethod(final JDefinedClass clazz, final String name, final String body) { final JMethod method = clazz.method(JMod.PUBLIC, Void.TYPE, name); method._throws(Exception.class); method.body().directStatement(body); } /** * Writes the generated classes to disk at the supplied path. * * @param path to write the classes to * * @throws IOException if the write fails */ public void write(final String path) throws IOException { final File f = new File(path); if (!f.exists()) { f.mkdirs(); } codeModel.build(f); // add imports final Path sourceDir = Paths.get(path); Files.walkFileTree(sourceDir, new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException { if (attrs.isRegularFile()) { String content = new String(Files.readAllBytes(file), StandardCharsets.UTF_8); content = content.replaceFirst( "package org.ldaptive.doc;", "package org.ldaptive.doc;\n\n" + IMPORT_STATEMENTS); Files.write(file, content.getBytes(StandardCharsets.UTF_8)); } return FileVisitResult.CONTINUE; } }); } /** * Provides command line access to a {@link ClassGenerator}. Expects two arguments: * * <ol> * <li>url to source zip file</li> * <li>target directory to write files to</li> * </ol> * * @param args command line arguments * * @throws Exception if any error occurs */ public static void main(final String[] args) throws Exception { final String url = args[0]; final String targetPath = args[1]; final ClassGenerator generator = new ClassGenerator(url, targetPath); generator.generate(); generator.write(targetPath + "/generated-test-sources/ldaptive-docs"); } }