/**
*
* Copyright 2010, Lawrence McAlpin.
*
*
* 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 play.modules.scaffold.generator;
import java.io.File;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import play.Logger;
import play.Play;
import play.exceptions.UnexpectedException;
import play.libs.IO;
import play.modules.scaffold.entity.Entity;
import play.modules.scaffold.utils.Strings;
import play.templates.Template;
import play.vfs.VirtualFile;
/**
* Creates boilerplate controllers and views for basic CRUD functions. This is
* useful if you don't want to use the CRUD plugin, because you intend to
* customize the output, but would still like to have basic CRUD functionality
* built out automagically.
*
* The scaffolding also creates a basic Application.index page which lists the
* controllers, as well as a default stylesheet and layout.
*
* @author Lawrence McAlpin
*/
public class ScaffoldingGenerator {
private static final String FORM_HTML = "_form";
private static final String CREATE_HTML = "create";
private static final String SHOW_HTML = "show";
private static final String EDIT_HTML = "edit";
private static final String LIST_HTML = "index";
private static final List<String> VIEW_HTMLS = Arrays.asList(FORM_HTML, CREATE_HTML, SHOW_HTML, EDIT_HTML,
LIST_HTML);
private List<Entity> entities = new ArrayList<Entity>();
private boolean forceOverwrite;
private boolean includeLayout;
private boolean includeLogin;
private boolean flattenPaths;
private String applicationName;
public ScaffoldingGenerator() {
applicationName = Play.configuration.getProperty("application.name", "Your New Application");
}
/**
* TemplateCompiler was a concrete class in Play 1.0. In Play 1.1, it became
* an abstract class to better support alternative template engines.
* GroovyTemplate is the class we use in Play 1.1.
*
* Because we want to support BOTH Play 1.0 and Play 1.1, we use reflection
* to invoke the correct template compiler.
*
* @param templateFile
* @param targetFile
* @param args
*/
public static void invokeTemplate(TargetFileType targetFileType, VirtualFile templateFile, File targetFile, Map<String, Object> args) {
try {
String version = Play.version;
Object templateCompiler = null;
Method compile = null;
if (version.startsWith("1.0")) {
Class<?> templateCompilerClass = Class.forName("play.templates.TemplateCompiler");
if (templateCompilerClass != null) {
compile = templateCompilerClass.getMethod("compile", VirtualFile.class);
}
templateCompiler = templateCompilerClass;
} else {
Class<?> templateCompilerClass = Class.forName("play.templates.GroovyTemplateCompiler");
if (templateCompilerClass != null) {
compile = templateCompilerClass.getMethod("compile", VirtualFile.class);
}
templateCompiler = templateCompilerClass.newInstance();
}
if (templateCompiler == null || templateCompiler == null) {
Logger.error("Error looking up the template compiler method");
System.exit(-1);
}
// Template template = TemplateCompiler.compile(templateFile);
Template template = (Template) compile.invoke(templateCompiler, templateFile);
String rawOutput = template.render(args);
String output = rawOutput;
if (targetFileType == TargetFileType.VIEW)
output = Strings.removeBlankLines(rawOutput);
IO.writeContent(output, targetFile);
} catch (UnexpectedException e) {
Logger.warn(e, "! Failed to generate output successfully: " + e.getMessage());
} catch (Throwable t) {
Logger.fatal(t, "! Unexpected error processing template: " + templateFile.getName(), args);
System.out.println(templateFile.contentAsString());
System.exit(-1);
}
}
public void addEntity(Entity entity) {
this.entities.add(entity);
}
public void generate() {
// copy over the main.html layout
if (isIncludeLayout()) {
generateLayout();
// create Application.index
generateHome();
}
if (includeLogin) {
generateLogin();
}
for (Entity entity : entities) {
generate(entity);
}
}
public void generate(Entity entity) {
// determine the name of the controller
// by convention, it is the plural of the entity name
generateController(entity);
// determine the name of the controller
// by convention, it is the plural of the entity name
generateViewsForEntity(entity);
}
private void invokeTemplate(TargetFileType targetFileType, String templatePath, String targetPath) {
invokeTemplate(targetFileType, templatePath, targetPath, new HashMap<String, Object>());
}
private void invokeTemplate(TargetFileType targetFileType, String templatePath, String targetPath, Map<String, Object> templateArgs) {
templateArgs.put("applicationName", applicationName);
templateArgs.put("includeLogin", includeLogin);
templateArgs.put("entities", entities);
// this represents the source file that we generate
VirtualFile targetFile = Play.getVirtualFile(targetPath);
// this represents the template we use to generate the source code
VirtualFile templateFile = Play.getVirtualFile(templatePath);
// if the source code already exists, skip it - we do not overwrite
// normally
if (targetFile == null || !targetFile.exists() || forceOverwrite) {
File fileToCreate = Play.getFile(targetPath);
if (templateFile == null || !templateFile.exists()) {
Logger.error("!! ERROR: Can't find scaffold template -- " + templatePath);
}
invokeTemplate(targetFileType, templateFile, fileToCreate, templateArgs);
Logger.info("+ " + targetPath);
} else {
Logger.info("! Skipping " + targetPath);
}
}
private void copyFile(TargetFileType type, String fileName) {
String[] paths = getPaths(type, fileName, fileName);
copyFile(paths[0], paths[1]);
}
private void generate(TargetFileType type, String fileName) {
String[] paths = getPaths(type, fileName, fileName);
invokeTemplate(type, paths[0], paths[1]);
}
private void generate(TargetFileType type, String sourceFileName, String targetName) {
String[] paths = getPaths(type, null, sourceFileName, targetName);
invokeTemplate(type, paths[0], paths[1]);
}
private void generate(TargetFileType type, String sourceFolderName, String sourceFileName, String targetName) {
String[] paths = getPaths(type, sourceFolderName, sourceFileName, targetName);
invokeTemplate(type, paths[0], paths[1]);
}
private void generateForEntity(Entity entity, TargetFileType type, String templateFolderPath,
String templateFileName, String targetName) {
if (!isFlattenPaths() && entity.getSubpackage() != null) {
String subpackage = entity.getSubpackage();
if (!subpackage.isEmpty()) {
subpackage = subpackage.replaceAll("\\.", "/");
targetName = subpackage + "/" + targetName;
}
}
String[] paths = getPaths(type, templateFolderPath, templateFileName, targetName);
Map<String, Object> templateArgs = new HashMap<String, Object>();
templateArgs.put("entity", entity);
templateArgs.put("subpackage", isFlattenPaths() ? "" : entity.getSubpackage().isEmpty() ? "" : "." + entity.getSubpackage());
invokeTemplate(type, paths[0], paths[1], templateArgs);
}
private String[] getPaths(TargetFileType type, String templateFileName, String targetFileName) {
return getPaths(type, null, templateFileName, targetFileName);
}
private String[] getPaths(TargetFileType type, String templateFolderPath, String templateFileName,
String targetFileName) {
String sourceFileName = templateFolderPath != null ? templateFolderPath + '/' + templateFileName.toLowerCase()
: templateFileName.toLowerCase();
StringBuilder baseTemplatePath = new StringBuilder("app" + '/' + "views" + '/' + "scaffold" + '/');
if (type != TargetFileType.LAYOUT) {
baseTemplatePath.append(type.getPath() + '/');
}
String templateFile = baseTemplatePath.toString() + sourceFileName + type.getSourceSuffix();
String targetPath = "app" + '/';
String additionalPath = type.getPath();
if (additionalPath.length() > 0)
targetPath = targetPath + additionalPath + '/';
String pathToTargetFile = targetFileName.contains("/") ? targetFileName.substring(0, targetFileName.lastIndexOf('/')) : "";
ensureDirectoryExists(targetPath + pathToTargetFile);
String targetFile = targetPath + targetFileName + type.getTargetSuffix();
return new String[] { templateFile, targetFile };
}
private void generateController(Entity entity) {
Class<?> controller = Play.classloader.getClassIgnoreCase(entity.getControllerName());
if (controller != null) {
Logger.info("Skipping controller: " + entity.getControllerName());
return;
}
Logger.info("Generating controller: " + entity.getControllerName());
generateForEntity(entity, TargetFileType.CONTROLLER, null, "controller", entity.getControllerName());
}
private void generateHome() {
generate(TargetFileType.VIEW, "Application", "index", "Application" + '/' + "index");
}
// generate the layout file
private void generateLayout() {
Logger.info("Generating layout: main.html");
generate(TargetFileType.LAYOUT, "main", "views" + '/' + "main");
}
private void copyFile(String sourcePath, String targetPath) {
VirtualFile templateFile = Play.getVirtualFile(sourcePath);
VirtualFile targetFile = Play.getVirtualFile(targetPath);
String templateLayout = templateFile.contentAsString();
if (targetFile == null || !targetFile.exists() || forceOverwrite) {
try {
File fileToCreate = Play.getFile(targetPath);
IO.writeContent(templateLayout, fileToCreate);
} catch (UnexpectedException e) {
Logger.error(e, "IO Exception");
} catch (Throwable t) {
Logger.error(t, "Unhandled Exception");
}
} else {
Logger.info("! Skipping " + targetPath);
}
}
private void generateLogin() {
// generate model for login handling
generate(TargetFileType.MODEL, "RoleType");
copyFile(TargetFileType.MODEL, "User");
// implement authentify
copyFile(TargetFileType.CONTROLLER, "Security");
// generate views and controller for User
Class<?> clazz;
try {
clazz = Play.classloader.loadClass("models.User");
if (clazz != null) {
Entity entity = new Entity(clazz);
if (!entities.contains(entity)) {
generateViewsForEntity(entity);
}
}
} catch (ClassNotFoundException e) {
Logger.error(e, "Can't load User class");
}
// copy Secure/login.html
String sourcePath = "app" + '/' + "views" + '/' + "scaffold" + '/' + "views" + '/' + "Secure" + '/'
+ "login.html";
String securePath = "app" + '/' + "views" + '/' + "Secure";
ensureDirectoryExists(securePath);
String targetPath = securePath + '/' + "login.html";
copyFile(sourcePath, targetPath);
}
private void generateViewsForEntity(Entity entity) {
// create the view folder if necessary
Logger.info("Generating views for " + entity.getControllerName());
for (String view : VIEW_HTMLS) {
generateForEntity(entity, TargetFileType.VIEW, "Entity", '/' + view, entity.getControllerName() + '/'
+ view);
}
}
private void ensureDirectoryExists(String templatePath) {
File viewPathDirectory = new File(templatePath);
if (!viewPathDirectory.exists()) {
viewPathDirectory.mkdirs();
}
}
public boolean isForceOverwrite() {
return forceOverwrite;
}
public boolean isIncludeLayout() {
return includeLayout;
}
public boolean isIncludeLogin() {
return includeLogin;
}
public boolean isFlattenPaths() {
return flattenPaths;
}
public void setFlattenPaths(boolean flattenPaths) {
this.flattenPaths = flattenPaths;
}
public void setForceOverwrite(boolean forceOverwrite) {
this.forceOverwrite = forceOverwrite;
}
public void setIncludeLayout(boolean includeLayout) {
this.includeLayout = includeLayout;
}
public void setIncludeLogin(boolean includeLogin) {
this.includeLogin = includeLogin;
}
}