com.legstar.protobuf.cobol.ProtoCobol.java Source code

Java tutorial

Introduction

Here is the source code for com.legstar.protobuf.cobol.ProtoCobol.java

Source

/*******************************************************************************
 * Copyright (c) 2012 LegSem EURL.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Lesser Public License v3
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/lgpl.html
 * 
 * Contributors:
 *     LegSem EURL - initial API and implementation
 ******************************************************************************/
/**
 * Based on wave-protocol, Copyright 2010 Google Inc.
 * Original author kalman@google.com (Benjamin Kalman)
 *
 * 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.legstar.protobuf.cobol;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;

import javax.tools.JavaCompiler;
import javax.tools.JavaCompiler.CompilationTask;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.google.protobuf.Descriptors.Descriptor;
import com.google.protobuf.Descriptors.FileDescriptor;
import com.legstar.cobol.gen.CopybookGenerator;
import com.legstar.cobol.model.CobolDataItem;
import com.legstar.protobuf.cobol.ProtoCobolMapper.HasMaxSize;
import com.legstar.protobuf.cobol.ProtoCobolUtils.ProtoFileJavaProperties;

/**
 * Translates protocol buffer protos files to COBOL artifacts.
 * <p/>
 * Generated artifacts are:
 * <ul>
 * <li>A copybook that represents the root protocol-buffer message structure</li>
 * <li>A parser subprogram that turns the protocol-buffer wire format to z/OS
 * data, conforming to the previous copybook</li>
 * <li>A writer subprogram that turns z/OS data, conforming to the previous
 * copybook, to the protocol-buffer wire format</li>
 * </ul>
 * <p/>
 * This is inspired and borrows code from the protobuf-stringtemplate (PST)
 * subproject in Google-Apache wave.
 * <p/>
 * There are 3 ways this generator can be invoked:
 * <ul>
 * <li>If you already have a protobuf FileDescriptor, then you can invoke the
 * run method directly</li>
 * <li>Otherwise, if you separately used protobuf to generate java code, then
 * you can refer to the java code which will be loaded from the classpath and a
 * FileDescriptor obtained by reflection</li>
 * <li>Otherwise you can pass a proto file and this will invoke the protoc
 * compiler (which must be on the Path) and then the java compiler to get to the
 * same point as the previous option</li>
 * </ul>
 * One a protoc-generated java class is available, this will map the descriptor
 * to COBOL structures.
 * 
 */
public class ProtoCobol {

    private static Log logger = LogFactory.getLog(ProtoCobol.class);

    /** Generated copybook file base name will have this suffix. */
    private static final String COBOL_MEMBER_COPYBOOK_SUFFIX = "C";

    /** Generated copybook file extension name. */
    private static final String COPYBOOK_FILE_EXTENSION = "cpy";

    /** Generated programs file extension name. */
    private static final String PROGRAM_FILE_EXTENSION = "cbl";

    /**
     * By default, don't wait more than this many seconds for a command to
     * execute.
     */
    private static final int DEFAULT_TIMEOUT = 10;

    private File outputDir;
    private String qualifiedClassName;
    private File protoFile;
    private File protoPath;
    private int timeout = DEFAULT_TIMEOUT;

    /** The generation parameters set. */
    private ProtoCobolConfig protoCobolConfig = new ProtoCobolConfig();

    /** The protobuf to COBOL mapping logic. */
    private ProtoCobolMapper cobolMapper = new ProtoCobolMapper();

    /**
     * The main entry point if you don't have a FileDescriptor (@see
     * run(FileDescriptor) otherwise).
     * <p/>
     * Either qualifiedClassName or protoFile must be provided depending on if
     * you already ran the protoc compiler for java or you expect this tool to
     * run it for you.
     * 
     * @throws ProtoCobolException if generation fails.
     */
    public void run() throws ProtoCobolException {

        if (outputDir == null) {
            throw new ProtoCobolException("outputDir cannot be null");
        }
        if (qualifiedClassName != null) {
            run(toFileDescriptor(qualifiedClassName));
        } else if (protoFile != null) {
            if (!protoFile.exists()) {
                throw new ProtoCobolException(
                        "The specified proto file: " + protoFile.getAbsolutePath() + ", does not exist");
            }
            run(toFileDescriptor(protoFile, protoPath, timeout));

        } else {
            throw new ProtoCobolException(
                    "You must specify either a proto file name or a protoc-generated java class name");
        }

    }

    /**
     * Run with a protobuf FileDescriptor.
     * <p/>
     * This will build a COBOL model by mapping the FileDescriptor content and
     * then uses StringTemplate to produce COBOL code.
     * 
     * @param fd the protobuf FileDescriptor
     * @throws ProtoCobolException if generation fails
     */
    public void run(FileDescriptor fd) throws ProtoCobolException {

        logger.info(
                "ProtoCobol started with file: " + fd.getName() + ", output dir: " + outputDir.getAbsolutePath());

        List<ProtoCobolException> exceptions = new ArrayList<ProtoCobolException>();

        try {
            for (Descriptor messageDescriptor : fd.getMessageTypes()) {

                ProtoCobolDataItem protoCobolDataItem = cobolMapper.toCobol(messageDescriptor);

                String copybookContent = CopybookGenerator.generate(protoCobolDataItem.getCobolDataItem());
                File copybookFile = writeCopybookFile(protoCobolDataItem.getCobolDataItem(), copybookContent);
                if (logger.isDebugEnabled()) {
                    logger.debug("Generated copy book in file: " + copybookFile.getPath());
                    logger.debug(copybookContent);
                }

                String parserContent = ProtoCobolGenerator.generateParser(protoCobolConfig, protoCobolDataItem);
                File parserFile = writeParserFile(protoCobolDataItem, parserContent);
                if (logger.isDebugEnabled()) {
                    logger.debug("Generated parser in file: " + parserFile.getPath());
                    logger.debug(parserContent);
                }

                String writerContent = ProtoCobolGenerator.generateWriter(protoCobolConfig, protoCobolDataItem);
                File writerFile = writeWriterFile(protoCobolDataItem, writerContent);
                if (logger.isDebugEnabled()) {
                    logger.debug("Generated writer in file: " + writerFile.getPath());
                    logger.debug(writerContent);
                }

            }
            logger.info("ProtoCobol succeeded");

        } catch (Exception e) {
            exceptions.add(new ProtoCobolException("COBOL generation failed", e));
        }

        for (ProtoCobolException e : exceptions) {
            logger.error("Generation error", e);
        }
        if (!exceptions.isEmpty()) {
            throw exceptions.get(0);
        }
    }

    /**
     * Write the COBOL copybook to a file.
     * 
     * @param cobolDataItem the data item
     * @param copybookContent the copybook content
     * @return a file named after the protobuf message mapped to COBOL
     * @throws IOException if writing fails
     */
    protected File writeCopybookFile(CobolDataItem cobolDataItem, String copybookContent) throws IOException {
        File copybookFile = new File(outputDir,
                cobolDataItem.getCobolName() + COBOL_MEMBER_COPYBOOK_SUFFIX + "." + COPYBOOK_FILE_EXTENSION);
        FileUtils.writeStringToFile(copybookFile, copybookContent);
        return copybookFile;
    }

    /**
     * Write the COBOL parser to a file.
     * 
     * @param protoCobolDataItem the decorated data item
     * @param parserContent the parser content
     * @return a file named after the generated parser program name
     * @throws IOException if writing fails
     */
    protected File writeParserFile(ProtoCobolDataItem protoCobolDataItem, String parserContent) throws IOException {
        File parserFile = new File(outputDir,
                protoCobolDataItem.getParserProgramName() + "." + PROGRAM_FILE_EXTENSION);
        FileUtils.writeStringToFile(parserFile, parserContent);
        return parserFile;
    }

    /**
     * Write the COBOL writer to a file.
     * 
     * @param protoCobolDataItem the decorated data item
     * @param writerContent the writer content
     * @return a file named after the generated writer program name
     * @throws IOException if writing fails
     */
    protected File writeWriterFile(ProtoCobolDataItem protoCobolDataItem, String writerContent) throws IOException {
        File writerFile = new File(outputDir,
                protoCobolDataItem.getWriterProgramName() + "." + PROGRAM_FILE_EXTENSION);
        FileUtils.writeStringToFile(writerFile, writerContent);
        return writerFile;
    }

    /**
     * Similar to wave PstFileDescriptor#asFileDescriptor.
     * <p/>
     * The class name is assumed to be available from the classpath.
     * 
     * @param qualifiedClassName the qualified class name
     * @return the File descriptor
     */
    public FileDescriptor toFileDescriptor(String qualifiedClassName) {
        try {
            Class<?> clazz = loadClass(qualifiedClassName);
            return toFileDescriptor(clazz);
        } catch (Exception e) {
            throw new IllegalArgumentException(
                    "Class " + qualifiedClassName + " cannot be found or was not generated by protobuf-java", e);
        }
    }

    /**
     * Similar to wave PstFileDescriptor#asFileDescriptor.
     * <p/>
     * Retrieve de FileDescriptor by reflecting on the protoc-generated java
     * class.
     * 
     * @param clazz the protoc-generated java class
     * @return the File descriptor
     */
    public FileDescriptor toFileDescriptor(Class<?> clazz) {
        try {
            Method method = clazz.getMethod("getDescriptor");
            return (FileDescriptor) method.invoke(null);
        } catch (Exception e) {
            throw new IllegalArgumentException(
                    "Class " + qualifiedClassName + " cannot be found or was not generated by protobuf-java", e);
        }
    }

    /**
     * Starting from a proto file, two steps are needed to produce the file
     * descriptor:
     * <ul>
     * <li>Invoke the protoc compiler to produce a java class source code</li>
     * <li>Invoke the java compiler to turn the source to binary</li>
     * </ul>
     * 
     * @param protoFile
     * @param protoPath
     * @param timeout
     * @return
     * @throws ProtoCobolException
     */
    public FileDescriptor toFileDescriptor(File protoFile, File protoPath, int timeout) throws ProtoCobolException {
        try {
            File javaOut = new File(FileUtils.getTempDirectory(), "ProtoCobol" + System.currentTimeMillis());
            javaOut.mkdirs();
            File javaSourceFile = runProtoJava(protoFile,
                    (protoPath == null) ? protoFile.getParentFile() : protoPath, javaOut, timeout);
            Class<?> clazz = runJavaCompiler(javaOut, javaSourceFile);
            FileDescriptor fileDescriptor = toFileDescriptor(clazz);
            FileUtils.forceDelete(javaOut);
            return fileDescriptor;
        } catch (IOException e) {
            throw new ProtoCobolException(e);
        }
    }

    /**
     * From Google's org.waveprotocol.pst.PstFileDescriptor.
     * <p/>
     * Call the protoc compiler, waiting a limited amount of time for
     * completion.
     * 
     * @param protoFile the protocol buffer file (proto file)
     * @param protoPath additional imported protocol buffer files will be picked
     *            up from this location
     * @param javaOut where, on the file system, the generated java class will
     *            be produced
     * @param timeout how long to wait before canceling (in seconds)
     * @return the generated java source file
     * @throws ProtoCobolException if invokation fails
     */
    public File runProtoJava(File protoFile, File protoPath, File javaOut, int timeout) throws ProtoCobolException {
        try {
            String[] protocCommand = new String[] { "protoc", protoFile.getAbsolutePath(),
                    "-I" + protoPath.getAbsolutePath(), "--java_out", javaOut.getAbsolutePath() };
            logger.info("About to execute: " + StringUtils.join(protocCommand, ' '));

            Process protoc = Runtime.getRuntime().exec(protocCommand);
            killProcessAfter(timeout, TimeUnit.SECONDS, protoc);
            int exitCode = protoc.waitFor();
            if (exitCode != 0) {
                throw new ProtoCobolException("Command failed. " + IOUtils.toString(protoc.getErrorStream()));

            }
            logger.info("Command succeeded. " + IOUtils.toString(protoc.getInputStream()));
            return locateJavaSourceFile(javaOut);
        } catch (IOException e) {
            throw new ProtoCobolException(e);
        } catch (InterruptedException e) {
            throw new ProtoCobolException(e);
        }
    }

    /**
     * After a java source file is generated, we need to locate it in the output
     * folder. The class probably belongs to a package and is located several
     * subdirectories below the output dir.
     * 
     * @param javaOut the output directory
     * @return the generated java source file
     * @throws ProtoCobolException if file cannot be located
     */
    protected File locateJavaSourceFile(File javaOut) throws ProtoCobolException {

        try {
            ProtoFileJavaProperties javaProperties = ProtoCobolUtils.getJavaProperties(protoFile);
            return new File(javaOut, ProtoCobolUtils.packageToLocation(javaProperties.getJavaPackageName())
                    + javaProperties.getJavaClassName());
        } catch (IOException e) {
            throw new ProtoCobolException(e);
        }
    }

    /**
     * Compile a java source file.
     * 
     * @param javaOut where, on the file system, the generated java class will
     *            be produced
     * @param javaSourceFile the java source file
     * @return the java class loaded
     * @throws ProtoCobolException if compilation fails
     */
    public Class<?> runJavaCompiler(File javaOut, File javaSourceFile) throws ProtoCobolException {

        logger.info("About to compile " + javaSourceFile.getAbsolutePath());

        JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();
        if (javaCompiler == null) {
            throw new ProtoCobolException("You need to have the JDK tools.jar on the classpath");
        }
        StandardJavaFileManager manager = javaCompiler.getStandardFileManager(null, null, null);
        Iterable<? extends JavaFileObject> units = manager.getJavaFileObjects(javaSourceFile.getAbsolutePath());
        String[] opts = new String[] { "-d", javaOut.getAbsolutePath() };
        CompilationTask task = javaCompiler.getTask(null, manager, null, Arrays.asList(opts), null, units);
        boolean status = task.call();
        if (status) {
            logger.info("Compilation successful");
        } else {
            throw new ProtoCobolException("Compilation failed for " + javaSourceFile.getAbsolutePath());
        }
        return loadClass(javaOut, getRelativeClassName(javaOut, javaSourceFile));
    }

    /**
     * Retrieve the java class file name relative to the base directory where it
     * was generated.
     * 
     * @param javaOut the base directory
     * @param javaSourceFile the java source file (the binary file is expected
     *            at the same location)
     * @return the java binary class name relative to the base directory
     */
    protected String getRelativeClassName(File javaOut, File javaSourceFile) {
        return javaSourceFile.getAbsolutePath().substring(javaOut.getAbsolutePath().length() + 1).replace(".java",
                ".class");
    }

    /**
     * From Google's org.waveprotocol.pst.PstFileDescriptor.
     * <p/>
     * Will kill a process if it takes too long.
     * 
     * @param delay how long to wait (
     * @param unit the unit of time delay is expressed in
     * @param process the process to kill
     */
    protected void killProcessAfter(final long delay, final TimeUnit unit, final Process process) {
        Thread processKiller = new Thread() {
            @Override
            public void run() {
                try {
                    Thread.sleep(unit.toMillis(delay));
                    process.destroy();
                } catch (InterruptedException e) {
                }
            }
        };
        processKiller.setDaemon(true);
        processKiller.start();
    }

    /**
     * From Google's org.waveprotocol.pst.PstFileDescriptor.
     * 
     * @param baseDir the base directory where compiled java binaries are
     *            located
     * @param path the path to the java binary file relative to the base
     *            directory
     * @return the java class loaded
     * @throws ProtoCobolException if class cannot be loaded
     */
    protected Class<?> loadClass(File baseDir, String path) throws ProtoCobolException {
        try {
            ClassLoader classLoader = new URLClassLoader(new URL[] { baseDir.toURI().toURL() });
            return classLoader.loadClass(getBinaryName(path));
        } catch (Exception e) {
            throw new ProtoCobolException(e);
        }
    }

    /**
     * Rather than using the Class.forName mechanism first, this uses
     * Thread.getContextClassLoader instead. In a Servlet context such as
     * Tomcat, this allows JAXB classes for instance to be loaded from the web
     * application (webapp) location while this code might have been loaded from
     * shared/lib. If Thread.getContextClassLoader fails to locate the class
     * then we give a last chance to Class.forName.
     * 
     * @param qualifiedClassName the class name to load
     * @return the class
     * @throws ClassNotFoundException if class is not accessible from any class
     *             loader
     */
    public static Class<?> loadClass(final String qualifiedClassName) throws ClassNotFoundException {
        ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
        if (contextClassLoader == null) {
            return Class.forName(qualifiedClassName);
        }
        try {
            return contextClassLoader.loadClass(qualifiedClassName);
        } catch (ClassNotFoundException e) {
            return Class.forName(qualifiedClassName);
        }
    }

    /**
     * From Google's org.waveprotocol.pst.PstFileDescriptor.
     * 
     * @param path the path to the java binary file relative to the base
     *            directory
     * @return the qualified java class name
     */
    protected String getBinaryName(String path) {
        return path.replace(File.separatorChar, '.').substring(0, path.length() - ".class".length());
    }

    /**
     * Location of file system where COBOL files are generated.
     * 
     * @return the outputDir a location of file system where COBOL files are
     *         generated
     */
    public File getOutputDir() {
        return outputDir;
    }

    /**
     * Location of file system where COBOL files are generated.
     * 
     * @param outputDir the location of file system where COBOL files are
     *            generated to set
     */
    public ProtoCobol setOutputDir(File outputDir) {
        this.outputDir = outputDir;
        return this;
    }

    /**
     * The protoc generated java qualified class name.
     * 
     * @return the qualifiedClassName the protoc generated java qualified class
     *         name
     */
    public String getQualifiedClassName() {
        return qualifiedClassName;
    }

    /**
     * The protoc generated java qualified class name.
     * 
     * @param qualifiedClassName the the protoc generated java qualified class
     *            name to set
     * @return this instance for chaining
     */
    public ProtoCobol setQualifiedClassName(String qualifiedClassName) {
        this.qualifiedClassName = qualifiedClassName;
        return this;
    }

    /**
     * The input proto file.
     * 
     * @return the input proto file
     */
    public File getProtoFile() {
        return protoFile;
    }

    /**
     * input proto file.
     * 
     * @param protoFileName the input proto file to set
     * @return this instance for chaining
     */
    public ProtoCobol setProtoFile(File protoFile) {
        this.protoFile = protoFile;
        return this;
    }

    /**
     * Imported protocol buffer files will be picked up from this location
     * 
     * @return the location where imported proto files are found
     */
    public File getProtoPath() {
        return protoPath;
    }

    /**
     * Imported protocol buffer files will be picked up from this location
     * 
     * @param protoPath the location where imported proto files are found to set
     * @return this instance for chaining
     */
    public ProtoCobol setProtoPath(File protoPath) {
        this.protoPath = protoPath;
        return this;
    }

    /**
     * Maximum time to wait for a command to execute (seconds).
     * 
     * @return the maximum time to wait for a command to execute (seconds)
     */
    public int getTimeout() {
        return timeout;
    }

    /**
     * Maximum time to wait for a command to execute (seconds).
     * 
     * @param timeout the maximum time to wait for a command to execute
     *            (seconds) to set
     * @return this instance for chaining
     */
    public ProtoCobol setTimeout(int timeout) {
        this.timeout = timeout;
        return this;
    }

    /**
     * Add a new size provider. This is a user provider code that will provide
     * values for string maximum sizes and arrays maximum number of items.
     * 
     * @param provider a size provider
     * @return this instance for chaining
     */
    public ProtoCobol addSizeProvider(HasMaxSize provider) {
        if (provider != null) {
            cobolMapper.addSizeProvider(provider);
        }
        return this;
    }

    /**
     * COBOL character code page (CCSID number) to be used for conversions.
     * 
     * @param cobolCodePage the code page (CCSID number) to set
     */
    public ProtoCobol setCobolCodePage(int cobolCodePage) {
        protoCobolConfig.setCobolCodePage(cobolCodePage);
        return this;
    }
}