/** * Copyright 2010 Google Inc. * * 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 org.waveprotocol.pst; import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Descriptors.FileDescriptor; import org.antlr.stringtemplate.StringTemplate; import org.antlr.stringtemplate.StringTemplateGroup; import org.waveprotocol.pst.model.Message; import org.waveprotocol.pst.model.MessageProperties; import org.waveprotocol.pst.style.Styler; import java.io.BufferedWriter; import java.io.File; import java.io.FileNotFoundException; import java.io.FileWriter; import java.io.IOException; import java.util.List; /** * PST stands for protobuf-stringtemplate. * * The tool allows arbitrary code generation given a series of string * templates, each passed the description of a protocol buffer file. This allows * protobuf- based file generation beyond what existing protocol compilers * (protoc, protostuff, etc) are capable of (without modification), using the * convenience and safety of <a href="http://stringtemplate.org">string * template</a>. * * A number of sample templates are bundles (in templates), so see these as * examples. These templates give a complete client/server JSON message stack: * <ul> * <li><em>message</em> is a common interface.</li> * <li><em>messageTestImpl</em> is a simple/pure Java in-memory implementation * of the interface, for testing.</li> * <li><em>messagePojoImpl</em> is a like messageTestImpl with JSON * serialization and deserialization using the Gson library.</li> * <li><em>messageServerImpl</em> is a protobuf-backed implementation, useful * for a multi-server environment where the efficient serialization of protocol * buffers is an advantage. JSON is also supported.</li> * <li><em>messageClientImpl</em> is an efficent javascript implementation for * use with GWT.</li> * </ul> * * There is no particular reason why PST can only be used to generate Java, for * example, a pure JS rather than GWT implementation of the client JSON message * component could be generated[1]. * * PST is implemented using the protocol buffer reflection generated by protoc * alongside the actual Message classes it generates; these are converted into * simple Java model objects with simple accessors suitable for accessing from * stringtemplate. * * The code generated by stringtemplate is then post-processed using a simple * custom code formatter since the output from stringtemplate can be hard to * humanly read (e.g. the indentation is unpredictable). * * [1] although, currently it is hardcoded in PST to generate .java files, and * the model has Java-centric methods. The code formatter also assumes that * it is run over a Java file. These all could be easily modified, however. * * @author kalman@google.com (Benjamin Kalman) */ public final class Pst { private final File outputDir; private final FileDescriptor fd; private final Styler styler; private final Iterable<File> templates; private final boolean saveBackups; private final boolean useInt52; /** * @param outputDir the base directory to write the generated files * @param fd the {@link FileDescriptor} of the protobuf to use (i.e. pass to * each string template) * @param styler the code styler to post-process generated code with * @param templates the collection of string templates to use * @param saveBackups whether to save intermediate generated files * @param useInt52 whether we use doubles to serialize 64-bit integers */ public Pst(File outputDir, FileDescriptor fd, Styler styler, Iterable<File> templates, boolean saveBackups, boolean useInt52) { this.outputDir = checkNotNull(outputDir, "outputDir cannot be null"); this.fd = checkNotNull(fd, "fd cannot be null"); this.styler = checkNotNull(styler, "styler cannot be null"); this.templates = checkNotNull(templates, "templates cannot be null"); this.saveBackups = saveBackups; this.useInt52 = useInt52; } /** * Runs the code generation for all templates. */ public void run() throws PstException { List<PstException.TemplateException> exceptions = Lists.newArrayList(); for (File template : templates) { try { MessageProperties properties = createProperties(template); String groupName = stripSuffix(".st", template.getName()); String templateName = properties.hasTemplateName() ? properties.getTemplateName() : ""; StringTemplateGroup group = new StringTemplateGroup(groupName + "Group", dir(template)); StringTemplate st = group.getInstanceOf(groupName); for (Descriptor messageDescriptor : fd.getMessageTypes()) { Message message = new Message(messageDescriptor, templateName, properties); st.reset(); st.setAttribute("m", message); write(st, new File( outputDir.getPath() + File.separator + message.getFullJavaType().replace('.', File.separatorChar) + "." + (properties.hasFileExtension() ? properties.getFileExtension() : "java"))); } } catch (Exception e) { exceptions.add(new PstException.TemplateException(template.getPath(), e)); } } if (!exceptions.isEmpty()) { throw new PstException(exceptions); } } /** * @return the path to the directory which contains a file, or just the path * to the file itself if it's already a directory */ private String dir(File f) { return f.isDirectory() ? f.getPath() : (Strings.isNullOrEmpty(f.getParent()) ? "." : f.getParent()); } private String stripSuffix(String suffix, String s) { return s.endsWith(suffix) ? s.substring(0, s.length() - suffix.length()) : s; } private void write(StringTemplate st, File output) throws IOException { output.getParentFile().mkdirs(); BufferedWriter writer = new BufferedWriter(new FileWriter(output)); try { writer.write(st.toString()); } finally { try { writer.close(); } catch (IOException e) { // If another exception is already propagating, we don't // want to throw a secondary one. // This means that exceptions on close() are ignored, // but what could usefully be done for a close() // exception anyway? } } styler.style(output, saveBackups); } private MessageProperties createProperties(File template) throws FileNotFoundException, IOException { File propertiesFile = new File(template.getParentFile().getPath() + File.separator + "properties"); MessageProperties properties = propertiesFile.exists() ? MessageProperties.createFromFile(propertiesFile) : MessageProperties.createEmpty(); properties.setUseInt52(useInt52); return properties; } }