/**
* Copyright 2014 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Eclipse Public License version 1.0, available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.jboss.forge.addon.angularjs;
import static org.jboss.forge.addon.angularjs.ResourceProvider.ANGULAR_JS;
import static org.jboss.forge.addon.angularjs.ResourceProvider.ANGULAR_RESOURCE_JS;
import static org.jboss.forge.addon.angularjs.ResourceProvider.ANGULAR_ROUTE_JS;
import static org.jboss.forge.addon.angularjs.ResourceProvider.BOOTSTRAP_CSS;
import static org.jboss.forge.addon.angularjs.ResourceProvider.BOOTSTRAP_JS;
import static org.jboss.forge.addon.angularjs.ResourceProvider.BOOTSTRAP_THEME_CSS;
import static org.jboss.forge.addon.angularjs.ResourceProvider.FORGE_LOGO_PNG;
import static org.jboss.forge.addon.angularjs.ResourceProvider.GLYPHICONS_EOT;
import static org.jboss.forge.addon.angularjs.ResourceProvider.GLYPHICONS_SVG;
import static org.jboss.forge.addon.angularjs.ResourceProvider.GLYPHICONS_TTF;
import static org.jboss.forge.addon.angularjs.ResourceProvider.GLYPHICONS_WOFF;
import static org.jboss.forge.addon.angularjs.ResourceProvider.JQUERY_JS;
import static org.jboss.forge.addon.angularjs.ResourceProvider.LANDING_VIEW;
import static org.jboss.forge.addon.angularjs.ResourceProvider.MAIN_CSS;
import static org.jboss.forge.addon.angularjs.ResourceProvider.MODERNIZR_JS;
import static org.jboss.forge.addon.angularjs.ResourceProvider.OFFCANVAS_JS;
import static org.jboss.forge.addon.angularjs.ResourceProvider.getEntityTemplates;
import static org.jboss.forge.addon.angularjs.ResourceProvider.getGlobalTemplates;
import static org.jboss.forge.addon.angularjs.ResourceProvider.getStatics;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.JarURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import javax.inject.Inject;
import javax.persistence.Id;
import org.jboss.forge.addon.facets.FacetFactory;
import org.jboss.forge.addon.javaee.cdi.CDIFacet;
import org.jboss.forge.addon.javaee.cdi.ui.CDISetupCommand;
import org.jboss.forge.addon.javaee.ejb.EJBFacet;
import org.jboss.forge.addon.javaee.ejb.ui.EJBSetupWizard;
import org.jboss.forge.addon.javaee.jpa.JPAFacet;
import org.jboss.forge.addon.javaee.jpa.ui.setup.JPASetupWizard;
import org.jboss.forge.addon.javaee.rest.RestFacet;
import org.jboss.forge.addon.javaee.rest.ui.RestSetupWizard;
import org.jboss.forge.addon.javaee.servlet.ServletFacet;
import org.jboss.forge.addon.javaee.servlet.ServletFacet_3_0;
import org.jboss.forge.addon.javaee.servlet.ServletFacet_3_1;
import org.jboss.forge.addon.javaee.servlet.ui.ServletSetupWizard;
import org.jboss.forge.addon.parser.java.facets.JavaSourceFacet;
import org.jboss.forge.addon.parser.java.resources.JavaResource;
import org.jboss.forge.addon.projects.Project;
import org.jboss.forge.addon.projects.facets.DependencyFacet;
import org.jboss.forge.addon.projects.facets.MetadataFacet;
import org.jboss.forge.addon.projects.facets.WebResourcesFacet;
import org.jboss.forge.addon.resource.FileResource;
import org.jboss.forge.addon.resource.Resource;
import org.jboss.forge.addon.resource.ResourceFactory;
import org.jboss.forge.addon.resource.ResourceFilter;
import org.jboss.forge.addon.scaffold.metawidget.MetawidgetInspectorFacade;
import org.jboss.forge.addon.scaffold.spi.AccessStrategy;
import org.jboss.forge.addon.scaffold.spi.ScaffoldGenerationContext;
import org.jboss.forge.addon.scaffold.spi.ScaffoldProvider;
import org.jboss.forge.addon.scaffold.spi.ScaffoldSetupContext;
import org.jboss.forge.addon.templates.TemplateFactory;
import org.jboss.forge.addon.templates.facets.TemplateFacet;
import org.jboss.forge.addon.text.Inflector;
import org.jboss.forge.addon.ui.command.UICommand;
import org.jboss.forge.addon.ui.result.NavigationResult;
import org.jboss.forge.addon.ui.result.navigation.NavigationResultBuilder;
import org.jboss.forge.addon.ui.util.Metadata;
import org.jboss.forge.roaster.model.JavaClass;
import org.jboss.forge.roaster.model.Member;
import org.jboss.forge.roaster.model.source.JavaClassSource;
import org.jboss.forge.roaster.model.source.JavaSource;
import org.jboss.shrinkwrap.descriptor.api.webapp30.WebAppDescriptor;
import org.metawidget.util.simple.StringUtils;
/**
* A {@link ScaffoldProvider} that generates AngularJS scaffolding from JPA entities. The generated scaffold is utilizes
* the REST (JAX-RS) resources generated by the Java EE addon.
*/
public class AngularScaffoldProvider implements ScaffoldProvider
{
private static final String BASE_PACKAGE = AngularScaffoldProvider.class.getPackage().getName();
public static final String SCAFFOLD_DIR = "/" + BASE_PACKAGE.replace('.', '/');
Project project;
@Inject
private FacetFactory facetFactory;
@Inject
private ResourceFactory resourceFactory;
@Inject
private TemplateFactory templateFactory;
@Inject
private Inflector inflector;
@Override
public String getName()
{
return "AngularJS";
}
@Override
public String getDescription()
{
return "Scaffold a RESTful service and an AngularJS client, from JPA entities";
}
@Override
public List<Resource<?>> setup(ScaffoldSetupContext setupContext)
{
setProject(setupContext.getProject());
String targetDir = setupContext.getTargetDirectory();
targetDir = (targetDir == null) ? "" : targetDir;
// Setup static resources.
ArrayList<Resource<?>> result = new ArrayList<>();
WebResourcesFacet web = project.getFacet(WebResourcesFacet.class);
ProcessingStrategy strategy = new CopyResourcesStrategy(web);
for (ScaffoldResource scaffoldResource : getStatics(targetDir, strategy)) {
result.add(scaffoldResource.generate());
}
return result;
}
@Override
@SuppressWarnings("unchecked")
public boolean isSetup(ScaffoldSetupContext setupContext)
{
Project project = setupContext.getProject();
String targetDir = setupContext.getTargetDirectory();
targetDir = targetDir == null ? "" : targetDir;
if (project.hasAllFacets(WebResourcesFacet.class, DependencyFacet.class, JPAFacet.class, EJBFacet.class,
CDIFacet.class, RestFacet.class))
{
WebResourcesFacet web = project.getFacet(WebResourcesFacet.class);
boolean areResourcesInstalled = web.getWebResource(targetDir + GLYPHICONS_SVG).exists()
&& web.getWebResource(targetDir + GLYPHICONS_EOT).exists()
&& web.getWebResource(targetDir + GLYPHICONS_SVG).exists()
&& web.getWebResource(targetDir + GLYPHICONS_TTF).exists()
&& web.getWebResource(targetDir + GLYPHICONS_WOFF).exists()
&& web.getWebResource(targetDir + FORGE_LOGO_PNG).exists()
&& web.getWebResource(targetDir + ANGULAR_RESOURCE_JS).exists()
&& web.getWebResource(targetDir + ANGULAR_ROUTE_JS).exists()
&& web.getWebResource(targetDir + ANGULAR_JS).exists()
&& web.getWebResource(targetDir + MODERNIZR_JS).exists()
&& web.getWebResource(targetDir + JQUERY_JS).exists()
&& web.getWebResource(targetDir + BOOTSTRAP_JS).exists()
&& web.getWebResource(targetDir + OFFCANVAS_JS).exists()
&& web.getWebResource(targetDir + MAIN_CSS).exists()
&& web.getWebResource(targetDir + BOOTSTRAP_CSS).exists()
&& web.getWebResource(targetDir + BOOTSTRAP_THEME_CSS).exists()
&& web.getWebResource(targetDir + LANDING_VIEW).exists();
return areResourcesInstalled;
}
return false;
}
@Override
public List<Resource<?>> generateFrom(ScaffoldGenerationContext generationContext)
{
setProject(generationContext.getProject());
String targetDir = generationContext.getTargetDirectory();
targetDir = (targetDir == null) ? "" : targetDir;
List<Resource<?>> result = new ArrayList<>();
Collection<Resource<?>> resources = generationContext.getResources();
for (Resource<?> resource : resources)
{
JavaSource<?> javaSource = null;
if (resource instanceof JavaResource)
{
JavaResource javaResource = (JavaResource) resource;
try
{
javaSource = javaResource.getJavaType();
}
catch (FileNotFoundException fileEx)
{
throw new IllegalStateException(fileEx);
}
}
else
{
continue;
}
JavaClassSource entity = (JavaClassSource) javaSource;
String resourceRootPath = getRootResourcePath(project);
// Fetch the REST resource path from the existing JAX-RS resource if found.
String entityResourcePath = parseResourcePath(entity);
// If the path is not available, construct a default one from the JPA entity name
// We'll let the user resolve the incorrect path later,
// if needed through regeneration of the JAX-RS resources.
String entityName = entity.getName();
if (entityResourcePath == null || entityResourcePath.isEmpty())
{
entityResourcePath = inflector.pluralize(entityName.toLowerCase());
}
entityResourcePath = trimSlashes(entityResourcePath);
// Inspect the JPA entity and obtain a list of inspection results. Every inspected property is represented as a
// Map<String,String> and all such inspection results are collated into a list.
MetawidgetInspectorFacade metawidgetInspectorFacade = new MetawidgetInspectorFacade(project);
InspectionResultProcessor angularResultEnhancer = new InspectionResultProcessor(project,
metawidgetInspectorFacade);
List<Map<String, String>> inspectionResults = metawidgetInspectorFacade.inspect(entity);
String entityId = angularResultEnhancer.fetchEntityId(entity, inspectionResults);
inspectionResults = angularResultEnhancer.enhanceResults(entity, inspectionResults);
MetadataFacet metadata = project.getFacet(MetadataFacet.class);
// TODO: Provide a 'utility' class for allowing transliteration across language naming schemes
// We need this to use contextual naming schemes instead of performing toLowerCase etc. in FTLs.
// Prepare the Freemarker data model
Map<String, Object> dataModel = new HashMap<>();
dataModel.put("entityName", entityName);
dataModel.put("pluralizedEntityName", inflector.pluralize(entityName));
dataModel.put("entityId", entityId);
dataModel.put("properties", inspectionResults);
dataModel.put("projectId", StringUtils.camelCase(metadata.getProjectName()));
dataModel.put("projectTitle", StringUtils.uncamelCase(metadata.getProjectName()));
dataModel.put("resourceRootPath", resourceRootPath);
dataModel.put("resourcePath", entityResourcePath);
dataModel.put("parentDirectories", getParentDirectories(targetDir));
// Process the Freemarker templates with the Freemarker data model and retrieve the generated resources from
// the registry.
WebResourcesFacet web = project.getFacet(WebResourcesFacet.class);
ProcessingStrategy strategy = new ProcessTemplateStrategy(web, resourceFactory, project, templateFactory, dataModel);
List<ScaffoldResource> scaffoldResources = getEntityTemplates(targetDir, entityName, strategy);
scaffoldResources.add(new ScaffoldResource("/views/detail.html.ftl", targetDir + "/views/" + entityName
+ "/detail.html", new DetailTemplateStrategy(web, resourceFactory, project, templateFactory, dataModel)));
scaffoldResources.add(new ScaffoldResource("/views/search.html.ftl", targetDir + "/views/" + entityName
+ "/search.html", new SearchTemplateStrategy(web, resourceFactory, project, templateFactory, dataModel)));
for (ScaffoldResource scaffoldResource : scaffoldResources) {
result.add(scaffoldResource.generate());
}
}
List<Resource<?>> indexResources = generateIndex(targetDir);
result.addAll(indexResources);
return result;
}
@Override
public NavigationResult getSetupFlow(ScaffoldSetupContext setupContext)
{
Project project = setupContext.getProject();
NavigationResultBuilder builder = NavigationResultBuilder.create();
List<Class<? extends UICommand>> setupCommands = new ArrayList<>();
if (!project.hasFacet(JPAFacet.class))
{
builder.add(JPASetupWizard.class);
}
if (!project.hasFacet(CDIFacet.class))
{
setupCommands.add(CDISetupCommand.class);
}
if (!project.hasFacet(EJBFacet.class))
{
setupCommands.add(EJBSetupWizard.class);
}
if (!project.hasFacet(ServletFacet.class))
{
// TODO: FORGE-1296. Ensure that this wizard only sets up Servlet 3.0+
setupCommands.add(ServletSetupWizard.class);
}
if (!project.hasFacet(RestFacet.class))
{
setupCommands.add(RestSetupWizard.class);
}
if(setupCommands.size() >0)
{
Metadata compositeSetupMetadata = Metadata.forCommand(setupCommands.get(0))
.name("Setup Facets")
.description("Setup all dependent facets for the AngularJS scaffold.");
builder.add(compositeSetupMetadata, setupCommands);
}
return builder.build();
}
@Override
public NavigationResult getGenerationFlow(ScaffoldGenerationContext generationContext)
{
NavigationResultBuilder builder = NavigationResultBuilder.create();
builder.add(ScaffoldableEntitySelectionWizard.class);
return builder.build();
}
@Override
public AccessStrategy getAccessStrategy()
{
return null;
}
private void setProject(Project project)
{
this.project = project;
}
/**
* Generates the application's index aka landing page, among others. All artifacts that are generated once per
* scaffolding run are generated here.
*
* @param targetDir The target directory for the generated scaffold artifacts.
* @return A list of generated {@link Resource}s
*/
public List<Resource<?>> generateIndex(String targetDir)
{
ArrayList<Resource<?>> result = new ArrayList<>();
/*
* TODO: Revert this change at a later date, if necessary. This is currently done to ensure that entities are
* picked up during invocation of the plugin from the Forge wizard in JBDS.
*/
ResourceFilter filter = new ResourceFilter()
{
@Override
public boolean accept(Resource<?> resource)
{
FileResource<?> file = (FileResource<?>) resource;
if (!file.isDirectory() || file.getName().equals("resources") || file.getName().equals("WEB-INF")
|| file.getName().equals("META-INF"))
{
return false;
}
return true;
}
};
WebResourcesFacet web = this.project.getFacet(WebResourcesFacet.class);
List<Resource<?>> resources = web.getWebResource(targetDir + "/views/").listResources(filter);
List<String> entityNames = new ArrayList<>();
List<String> pluralizedEntityNames = new ArrayList<>();
for (Resource<?> resource : resources)
{
String resourceName = resource.getName();
entityNames.add(resourceName);
pluralizedEntityNames.add(inflector.pluralize(resourceName));
}
MetadataFacet metadata = project.getFacet(MetadataFacet.class);
Map<String, Object> dataModel = new HashMap<>();
dataModel.put("entityNames", entityNames);
dataModel.put("pluralizedEntityNames", pluralizedEntityNames);
dataModel.put("projectId", StringUtils.camelCase(metadata.getProjectName()));
dataModel.put("projectTitle", StringUtils.uncamelCase(metadata.getProjectName()));
dataModel.put("targetDir", targetDir);
ProcessingStrategy strategy = new ProcessTemplateStrategy(web, resourceFactory, project, templateFactory, dataModel);
for (ScaffoldResource scaffoldResource : getGlobalTemplates(targetDir, strategy)) {
result.add(scaffoldResource.generate());
}
configureWelcomeFile();
return result;
}
/**
* Configures the welcome file entry in the project's web application descriptor to the static ever-present
* <code>index.html</code> file. This method adds the entry only if it is absent.
*/
private void configureWelcomeFile()
{
String indexFileEntry = "/index.html";
ServletFacet servlet = this.project.getFacet(ServletFacet.class);
if (servlet instanceof ServletFacet_3_0)
{
WebAppDescriptor servletConfig = (WebAppDescriptor) servlet.getConfig();
servletConfig.getOrCreateWelcomeFileList().welcomeFile(indexFileEntry);
servlet.saveConfig(servletConfig);
}
else if (servlet instanceof ServletFacet_3_1)
{
org.jboss.shrinkwrap.descriptor.api.webapp31.WebAppDescriptor servletConfig = (org.jboss.shrinkwrap.descriptor.api.webapp31.WebAppDescriptor) servlet
.getConfig();
servletConfig.getOrCreateWelcomeFileList().welcomeFile(indexFileEntry);
servlet.saveConfig(servletConfig);
}
return;
}
/**
* Installs the templates into src/main/templates. All Freemarker templates would be copied into the
* src/main/templates/angularjs directory, obeying the same structure as the one in this provider.
*/
private void installTemplates()
{
// Install the required facet so that the templates directory is created if not present.
if (!project.hasFacet(TemplateFacet.class))
{
facetFactory.install(project, TemplateFacet.class);
}
TemplateFacet templates = project.getFacet(TemplateFacet.class);
// Obtain a reference to the scaffold directory in the classpath
URL resource = getClass().getClassLoader().getResource("scaffold");
if (resource != null && resource.getProtocol().equals("jar"))
{
try
{
// Obtain a reference to the JAR containing the scaffold directory
JarURLConnection connection = (JarURLConnection) resource.openConnection();
JarFile jarFile = connection.getJarFile();
Enumeration<JarEntry> entries = jarFile.entries();
// Iterate through the JAR entries and copy files to the template directory. Only files ending with .ftl,
// and
// present in the scaffold/ directory are copied.
while (entries.hasMoreElements())
{
JarEntry jarEntry = entries.nextElement();
String entryName = jarEntry.getName();
if (entryName.startsWith("scaffold/") && entryName.endsWith(".ftl"))
{
String relativeFilename = entryName.substring("scaffold/".length());
InputStream is = jarFile.getInputStream(jarEntry);
// Copy the file into a sub-directory under src/main/templates named after the scaffold provider.
Resource<File> templateResource = resourceFactory.create(new File(relativeFilename));
FileResource<?> fileResource = templateResource.reify(FileResource.class);
fileResource.setContents(is);
}
}
}
catch (IOException ioEx)
{
throw new RuntimeException(ioEx);
}
}
}
private String parseResourcePath(JavaClass klass)
{
JavaSourceFacet java = project.getFacet(JavaSourceFacet.class);
ResourcePathVisitor visitor = new ResourcePathVisitor(klass.getName());
java.visitJavaSources(visitor);
return visitor.getPath();
}
/**
* Obtains the root path for REST resources so that the AngularJS resource factory will be generated with the correct
* REST resource URL.
*
* @return The root path of the REST resources generated by the Forge REST plugin.
*/
private String getRootResourcePath(Project project)
{
RestFacet rest = project.getFacet(RestFacet.class);
String resourceRootPath = trimSlashes(rest.getApplicationPath());
return resourceRootPath;
}
/**
* Provided a target directory, this method calculates the parent directories to re-create the path to the web
* resource root.
*
* @param targetDir The target directory that would be used as the basis for calculating the parent directories.
* @return The parent directories to traverse. Represented as a sequence of '..' characters with '/' to denote
* multiple parent directories.
*/
private String getParentDirectories(String targetDir)
{
if (targetDir == null || targetDir.isEmpty())
{
return "";
}
else
{
targetDir = trimSlashes(targetDir);
int parents = countOccurrences(targetDir, '/') + 1;
StringBuilder parentDirectories = new StringBuilder();
for (int ctr = 0; ctr < parents; ctr++)
{
parentDirectories.append("../");
}
return parentDirectories.toString();
}
}
private int countOccurrences(String searchString, char charToSearch)
{
int count = 0;
for (int ctr = 0; ctr < searchString.length(); ctr++)
{
if (searchString.charAt(ctr) == charToSearch)
{
count++;
}
}
return count;
}
private String trimSlashes(String aString)
{
if (aString.startsWith("/"))
{
aString = aString.substring(1);
}
if (aString.endsWith("/"))
{
aString = aString.substring(0, aString.length() - 1);
}
return aString;
}
}