/*
* Copyright 2012 Robert W. Vawter III <bob@vawter.org>
*
* 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.jsonddl.generator;
import static org.jsonddl.generator.industrial.IndustrialDialect.getterName;
import java.io.IOException;
import java.io.Writer;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.jsonddl.model.Kind;
import org.jsonddl.model.Model;
import org.jsonddl.model.ModelVisitor;
import org.jsonddl.model.Property;
import org.jsonddl.model.Schema;
import org.jsonddl.model.Type;
import org.stringtemplate.v4.AttributeRenderer;
import org.stringtemplate.v4.AutoIndentWriter;
import org.stringtemplate.v4.Interpreter;
import org.stringtemplate.v4.ST;
import org.stringtemplate.v4.STGroup;
import org.stringtemplate.v4.STGroupFile;
import org.stringtemplate.v4.misc.ObjectModelAdaptor;
import org.stringtemplate.v4.misc.STNoSuchPropertyException;
/**
* A utility base class for generators that use StringTemplate.
* <p>
* Defines:
* <ul>
* <li>A {@value #NOW_TEMPLATE_NAME} template with a stable timestamp.</li>
* <li>A {@value #NAMES_DICTIONARY_NAME} dictionary with class names returned from
* {@link #getTemplateClasses()}.</li>
* </ul>
*/
public abstract class TemplateDialect implements Dialect {
private static final String DIALECT_PROPERTIES_KEY = "dialectProperties";
private static final String GENERATED_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss";
private static final int LINE_WIDTH = 80;
private static final String MODEL_KEY = "model";
private static final String NAMES_DICTIONARY_NAME = "names";
private static final String NOW_TEMPLATE_NAME = "now";
private STGroup templates;
@Override
public void generate(Options options, Collector output, final Schema s) throws IOException {
templates = loadTemplateGroup();
// Redefine now()
templates.undefineTemplate(NOW_TEMPLATE_NAME);
templates.defineTemplate(NOW_TEMPLATE_NAME,
new SimpleDateFormat(GENERATED_DATE_FORMAT).format(new Date()));
// Provide the collection of well-known classes to the template
Map<String, Object> classMap = new HashMap<String, Object>();
for (Class<?> clazz : getTemplateClasses()) {
classMap.put(clazz.getSimpleName(), clazz.getCanonicalName());
}
for (String name : getTemplateClassNames()) {
int idx = name.lastIndexOf('.');
if (idx == -1) {
classMap.put(name, name);
} else {
classMap.put(name.substring(idx + 1), name);
}
}
classMap.put(Dialect.class.getSimpleName(), getClass().getCanonicalName());
templates.defineDictionary(NAMES_DICTIONARY_NAME, classMap);
// Stringifies a Type as its parameterized, qualified source name
templates.registerRenderer(Type.class, new AttributeRenderer() {
@Override
public String toString(Object o, String formatString, Locale locale) {
return TypeAnswers.getParameterizedQualifiedSourceName((Type) o);
}
});
templates.registerModelAdaptor(Model.class, new ObjectModelAdaptor() {
@Override
public Object getProperty(Interpreter interp, ST self, Object o, Object property,
String propertyName) throws STNoSuchPropertyException {
if ("referencedModels".equals(propertyName)) {
final Set<Model> referencedModels = new LinkedHashSet<Model>();
((Model) o).accept(new ModelVisitor() {
@Override
public void endVisit(Type x, Context<Type> ctx) throws Exception {
if (Kind.DDL.equals(x.getKind())) {
referencedModels.add(s.getModels().get(x.getName()));
}
}
});
// A type shouldn't reference itself
referencedModels.remove(o);
return referencedModels;
}
return super.getProperty(interp, self, o, property, propertyName);
}
});
// Add a magic "getterName" property to Property objects for use by the templates
templates.registerModelAdaptor(Property.class, new ObjectModelAdaptor() {
@Override
public Object getProperty(Interpreter interp, ST self, Object o, Object property,
String propertyName) throws STNoSuchPropertyException {
if ("getterName".equals(propertyName)) {
return getterName(((Property) o).getName());
}
return super.getProperty(interp, self, o, property, propertyName);
}
});
// Map TypeAnswers methods onto Type objects
templates.registerModelAdaptor(Type.class, new ObjectModelAdaptor() {
@Override
public Object getProperty(Interpreter interp, ST self, Object o, Object property,
String propertyName) throws STNoSuchPropertyException {
if ("nestedKinds".equals(propertyName)) {
// A list of the inner kind parameterizations
final List<Kind> kindReferences = new ArrayList<Kind>();
((Type) o).accept(new ModelVisitor() {
@Override
public boolean visit(Type t, Context<Type> ctx) {
kindReferences.add(t.getKind());
return true;
}
});
kindReferences.remove(0);
return kindReferences;
}
if ("shouldProtect".equals(propertyName)) {
return TypeAnswers.shouldProtect((Type) o);
}
if (propertyName.startsWith("isKind")) {
String kindName = propertyName.substring("isKind".length());
Kind kind = Kind.valueOf(kindName.toUpperCase());
return kind.equals(((Type) o).getKind());
}
return super.getProperty(interp, self, o, property, propertyName);
}
});
doGenerate(options, output, s);
}
protected abstract void doGenerate(Options options, Collector output, Schema s)
throws IOException;
/**
* Configure the given template with {@code model} and {@code dialectProperties} attributes. If
* the model sets a dialect-specific {@code inspect} property to {@code true}, then
* {@link ST#inspect()} will be called.
*/
protected ST forModel(ST template, Model model) {
Set<String> attributeNames = template.getAttributes().keySet();
boolean inspect = false;
if (attributeNames.contains(DIALECT_PROPERTIES_KEY)) {
template.remove(DIALECT_PROPERTIES_KEY);
Map<String, Map<String, String>> dialectProperties = model.getDialectProperties();
if (dialectProperties != null) {
Map<String, String> properties = dialectProperties.get(getName());
if (properties != null) {
template.add(DIALECT_PROPERTIES_KEY, properties);
inspect = Boolean.parseBoolean(properties.get("inspect"));
}
}
}
if (attributeNames.contains(MODEL_KEY)) {
template.remove(MODEL_KEY);
template.add(MODEL_KEY, model);
}
if (inspect) {
template.inspect(LINE_WIDTH);
}
return template;
}
/**
* Returns the named template, attaching the {@code options} attribute.
*/
protected ST getTemplate(String name, Options options) {
return templates.getInstanceOf(name).add("options", options);
}
/**
* Subclasses may return a list of classes that will be defined in a {@code names} dictionary in
* the templates returned from {@link #getTemplate}. The simple name of the class will be mapped
* to the canonical name of the class, allowing templates to write {@code <names.JsonDdlObject>}
* instead of the fully-qualified type name or having to rely on import statements.
*/
protected List<Class<?>> getTemplateClasses() {
return Collections.emptyList();
}
/**
* Similar to {@link #getTemplateClasses()}, but uses string values to avoid generator
* dependencies on external toolkits.
*/
protected List<String> getTemplateClassNames() {
return Collections.emptyList();
}
protected STGroup getTemplateGroup() {
return templates;
}
/**
* Loads a template group based on dialect name.
*/
protected STGroup loadTemplateGroup() {
String location = "org/jsonddl/generator/templates/" + getName() + ".stg";
URL resource = Thread.currentThread().getContextClassLoader().getResource(location);
if (resource == null) {
// Try this class's classloader, using an absolute resource path
resource = getClass().getResource("/" + location);
}
if (resource == null) {
throw new RuntimeException("Could not locate template at " + location);
}
return new STGroupFile(resource, "UTF8", '<', '>');
}
/**
* Render the fully-initialized template into the given Writer. The line width will be set to
* {@value #LINE_WIDTH}. The Writer will be closed by this method.
*/
protected void renderTemplate(ST template, Writer out) throws IOException {
AutoIndentWriter writer = new AutoIndentWriter(out);
writer.setLineWidth(LINE_WIDTH);
template.write(writer);
out.close();
}
}