/*
 * Copyright 2012 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.scaffold.faces;

import static org.jboss.forge.scaffold.constants.ScaffoldConstants.INSTALLING_SCAFFOLD;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.enterprise.event.Event;
import javax.inject.Inject;
import javax.persistence.CascadeType;
import javax.persistence.Id;
import javax.persistence.OneToOne;

import org.jboss.forge.env.Configuration;
import org.jboss.forge.parser.JavaParser;
import org.jboss.forge.parser.java.Annotation;
import org.jboss.forge.parser.java.Field;
import org.jboss.forge.parser.java.FieldHolder;
import org.jboss.forge.parser.java.JavaClass;
import org.jboss.forge.parser.java.Member;
import org.jboss.forge.parser.java.Method;
import org.jboss.forge.parser.java.Parameter;
import org.jboss.forge.parser.xml.Node;
import org.jboss.forge.parser.xml.XMLParser;
import org.jboss.forge.project.Project;
import org.jboss.forge.project.dependencies.Dependency;
import org.jboss.forge.project.dependencies.DependencyBuilder;
import org.jboss.forge.project.facets.BaseFacet;
import org.jboss.forge.project.facets.DependencyFacet;
import org.jboss.forge.project.facets.FacetNotFoundException;
import org.jboss.forge.project.facets.JavaSourceFacet;
import org.jboss.forge.project.facets.WebResourceFacet;
import org.jboss.forge.project.facets.events.InstallFacets;
import org.jboss.forge.resources.FileResource;
import org.jboss.forge.resources.Resource;
import org.jboss.forge.resources.ResourceFilter;
import org.jboss.forge.scaffold.AccessStrategy;
import org.jboss.forge.scaffold.ScaffoldProvider;
import org.jboss.forge.scaffold.TemplateStrategy;
import org.jboss.forge.scaffold.faces.metawidget.config.ForgeConfigReader;
import org.jboss.forge.scaffold.util.ScaffoldUtil;
import org.jboss.forge.shell.ShellMessages;
import org.jboss.forge.shell.ShellPrintWriter;
import org.jboss.forge.shell.ShellPrompt;
import org.jboss.forge.shell.plugins.Alias;
import org.jboss.forge.shell.plugins.Help;
import org.jboss.forge.shell.plugins.RequiresFacet;
import org.jboss.forge.shell.util.Streams;
import org.jboss.forge.spec.javaee.CDIFacet;
import org.jboss.forge.spec.javaee.EJBFacet;
import org.jboss.forge.spec.javaee.FacesAPIFacet;
import org.jboss.forge.spec.javaee.FacesFacet;
import org.jboss.forge.spec.javaee.PersistenceFacet;
import org.jboss.forge.spec.javaee.ServletFacet;
import org.jboss.seam.render.TemplateCompiler;
import org.jboss.seam.render.spi.TemplateResolver;
import org.jboss.seam.render.template.CompiledTemplateResource;
import org.jboss.seam.render.template.resolver.ClassLoaderTemplateResolver;
import org.jboss.shrinkwrap.descriptor.api.spec.servlet.web.WebAppDescriptor;
import org.metawidget.statically.StaticMetawidget;
import org.metawidget.statically.StaticUtils.IndentedWriter;
import org.metawidget.statically.StaticWidget;
import org.metawidget.statically.faces.StaticFacesUtils;
import org.metawidget.statically.faces.component.html.StaticHtmlMetawidget;
import org.metawidget.statically.faces.component.html.widgetbuilder.HtmlOutcomeTargetLink;
import org.metawidget.statically.faces.component.html.widgetbuilder.ReadOnlyWidgetBuilder;
import org.metawidget.statically.faces.component.html.widgetbuilder.richfaces.RichFacesWidgetBuilder;
import org.metawidget.statically.html.widgetbuilder.HtmlTag;
import org.metawidget.statically.javacode.StaticJavaMetawidget;
import org.metawidget.util.ArrayUtils;
import org.metawidget.util.ClassUtils;
import org.metawidget.util.CollectionUtils;
import org.metawidget.util.XmlUtils;
import org.metawidget.util.simple.StringUtils;
import org.metawidget.widgetbuilder.composite.CompositeWidgetBuilder;
import org.metawidget.widgetbuilder.composite.CompositeWidgetBuilderConfig;
import org.metawidget.widgetbuilder.iface.WidgetBuilder;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;

/**
 * Facet to generate a Java Server Faces UI.
 * <p>
 * This facet utilizes <a href="http://metawidget.org">Metawidget</a> internally. This enables the use of the Metawidget
 * SPI (pluggable WidgetBuilders, Layouts etc) for customizing the generated User Interface. For more information on
 * writing Metawidget plugins, see <a href="http://metawidget.org/documentation.php">the Metawidget documentation</a>.
 * <p>
 * This Facet does <em>not</em> require Metawidget to be in the final project.
 *
 * @author <a href="mailto:lincolnbaxter@gmail.com">Lincoln Baxter, III</a>
 * @author Richard Kennard
 */
@Alias("faces")
@Help("JavaServer Faces scaffolding")
@RequiresFacet({ WebResourceFacet.class,
         DependencyFacet.class,
         PersistenceFacet.class,
         EJBFacet.class,
         CDIFacet.class,
         FacesAPIFacet.class })
public class FacesScaffold extends BaseFacet implements ScaffoldProvider
{
   //
   // Private statics
   //
   private static final String JBOSS_COMMUNITY_PNG = "/resources/jboss-community.png";
   private static final String TRUE_PNG = "/resources/true.png";
   private static final String SEARCH_PNG = "/resources/search.png";
   private static final String REMOVE_PNG = "/resources/remove.png";
   private static final String FORGE_STYLE_CSS = "/resources/forge-style.css";
   private static final String FORGE_LOGO_PNG = "/resources/forge-logo.png";
   private static final String FAVICON_ICO = "/resources/favicon.ico";
   private static final String FALSE_PNG = "/resources/false.png";
   private static final String BOOTSTRAP_CSS = "/resources/bootstrap.css";
   private static final String ADD_PNG = "/resources/add.png";
   private static final String ERROR_XHTML = "error.xhtml";
   private static final String XMLNS_PREFIX = "xmlns:";
   private static final String BACKING_BEAN_TEMPLATE = "scaffold/faces/BackingBean.jv";
   private static final String VIEW_UTILS_TEMPLATE = "scaffold/faces/ViewUtils.jv";
   private static final String TAGLIB_TEMPLATE = "scaffold/faces/forge.taglib.xml";
   private static final String VIEW_TEMPLATE = "scaffold/faces/view.xhtml";
   private static final String CREATE_TEMPLATE = "scaffold/faces/create.xhtml";
   private static final String SEARCH_TEMPLATE = "scaffold/faces/search.xhtml";
   private static final String NAVIGATION_TEMPLATE = "scaffold/faces/pageTemplate.xhtml";
   private static final String ERROR_TEMPLATE = "scaffold/faces/error.xhtml";
   private static final String INDEX_TEMPLATE = "scaffold/faces/index.xhtml";
   private static final String INDEX_WELCOME_TEMPLATE = "scaffold/faces/index.html";
   private final Dependency richfaces3UI = DependencyBuilder.create("org.richfaces.ui:richfaces-ui");
   private final Dependency richfaces3Impl = DependencyBuilder.create("org.richfaces.framework:richfaces-impl");
   private final Dependency richfaces4UI = DependencyBuilder.create("org.richfaces.ui:richfaces-components-ui");
   private final Dependency richfaces4Impl = DependencyBuilder.create("org.richfaces.core:richfaces-core-impl");
   //
   // Protected members (nothing is private, to help subclassing)
   //
   protected CompiledTemplateResource backingBeanTemplate;
   protected int backingBeanTemplateQbeMetawidgetIndent;
   protected int backingBeanTemplateRmEntityMetawidgetIndent;
   protected CompiledTemplateResource viewUtilsTemplate;
   protected CompiledTemplateResource taglibTemplate;
   protected CompiledTemplateResource viewTemplate;
   protected Map<String, String> viewTemplateNamespaces;
   protected int viewTemplateEntityMetawidgetIndent;
   protected CompiledTemplateResource createTemplate;
   protected Map<String, String> createTemplateNamespaces;
   protected int createTemplateEntityMetawidgetIndent;
   protected CompiledTemplateResource searchTemplate;
   protected Map<String, String> searchTemplateNamespaces;
   protected int searchTemplateSearchMetawidgetIndent;
   protected int searchTemplateBeanMetawidgetIndent;
   protected CompiledTemplateResource navigationTemplate;
   protected int navigationTemplateIndent;
   protected CompiledTemplateResource errorTemplate;
   protected CompiledTemplateResource indexWelcomeTemplate;
   protected CompiledTemplateResource indexTemplate;
   protected TemplateResolver<ClassLoader> resolver;
   protected final ShellPrompt prompt;
   protected final TemplateCompiler compiler;
   protected final Event<InstallFacets> install;
   protected StaticHtmlMetawidget entityMetawidget;
   protected StaticHtmlMetawidget searchMetawidget;
   protected StaticHtmlMetawidget beanMetawidget;
   protected StaticJavaMetawidget qbeMetawidget;
   protected StaticJavaMetawidget rmEntityMetawidget;

   private Configuration config;
   private ShellPrintWriter writer;

   //
   // Constructor
   //

   @Inject
   public FacesScaffold(final Configuration config,
            final ShellPrompt prompt,
            final TemplateCompiler compiler,
            final Event<InstallFacets> install,
            final ShellPrintWriter writer)
   {
      this.config = config;
      this.prompt = prompt;
      this.compiler = compiler;
      this.install = install;
      this.writer = writer;

      this.resolver = new ClassLoaderTemplateResolver(FacesScaffold.class.getClassLoader());

      if (this.compiler != null)
      {
         this.compiler.getTemplateResolverFactory().addResolver(this.resolver);
      }
   }

   //
   // Public methods
   //
   @Override
   public List<Resource<?>> setup(String targetDir, final Resource<?> template, final boolean overwrite)
   {
      List<Resource<?>> resources = generateIndex(targetDir, template, overwrite);
      setupWebXML();

      return resources;
   }

   /**
    * Overridden to setup the Metawidgets.
    * <p>
    * Metawidgets must be configured per project <em>and per Forge invocation</em>. It is not sufficient to simply
    * configure them in <code>setup</code> because the user may restart Forge and not run <code>scaffold setup</code> a
    * second time.
    */
   @Override
   public void setProject(Project project)
   {
      super.setProject(project);

      resetMetaWidgets();

   }

   private void resetMetaWidgets()
   {
      ForgeConfigReader configReader = new ForgeConfigReader(this.config, this.project);

      this.entityMetawidget = new StaticHtmlMetawidget();
      this.entityMetawidget.setConfigReader(configReader);
      this.entityMetawidget.setConfig("scaffold/faces/metawidget-entity.xml");

      this.searchMetawidget = new StaticHtmlMetawidget();
      this.searchMetawidget.setConfigReader(configReader);
      this.searchMetawidget.setConfig("scaffold/faces/metawidget-search.xml");

      this.beanMetawidget = new StaticHtmlMetawidget();
      this.beanMetawidget.setConfigReader(configReader);
      this.beanMetawidget.setConfig("scaffold/faces/metawidget-bean.xml");

      this.qbeMetawidget = new StaticJavaMetawidget();
      this.qbeMetawidget.setConfigReader(configReader);
      this.qbeMetawidget.setConfig("scaffold/faces/metawidget-qbe.xml");
      
      this.rmEntityMetawidget = new StaticJavaMetawidget();
      this.rmEntityMetawidget.setConfigReader(configReader);
      this.rmEntityMetawidget.setConfig("scaffold/faces/metawidget-remove-entity.xml");
   }

   @Override
   public List<Resource<?>> generateFromEntity(String targetDir, final Resource<?> template, final JavaClass entity,
            final boolean overwrite)
   {
      if(!isScaffoldable(entity))
      {
         ShellMessages.info(writer, "Skipping @Entity Java resource [" + entity.getQualifiedName() + "]");
         return Collections.emptyList();
      }
      
      resetMetaWidgets();

      // FORGE-460: setupRichFaces during generateFromEntity, not during setup, as generally 'richfaces setup' is called
      // *after* 'scaffold setup'
      setupRichFaces();

      // Track the list of resources generated

      List<Resource<?>> result = new ArrayList<Resource<?>>();
      try
      {
         JavaSourceFacet java = this.project.getFacet(JavaSourceFacet.class);
         WebResourceFacet web = this.project.getFacet(WebResourceFacet.class);

         loadTemplates();
         Map<Object, Object> context = CollectionUtils.newHashMap();
         context.put("entity", entity);
         String ccEntity = StringUtils.decapitalize(entity.getName());
         context.put("ccEntity", ccEntity);
         context.put("rmEntity", ccEntity + "ToDelete");
         setPrimaryKeyMetaData(context, entity);

         // Prepare qbeMetawidget
         this.qbeMetawidget.setPath(entity.getQualifiedName());
         StringWriter stringWriter = new StringWriter();
         this.qbeMetawidget.write(stringWriter, this.backingBeanTemplateQbeMetawidgetIndent);
         context.put("qbeMetawidget", stringWriter.toString().trim());
         
         // Prepare removeEntityMetawidget
         this.rmEntityMetawidget.setPath(entity.getQualifiedName());
         stringWriter = new StringWriter();
         this.rmEntityMetawidget.write(stringWriter, this.backingBeanTemplateRmEntityMetawidgetIndent);
         context.put("rmEntityMetawidget", stringWriter.toString().trim());
         
         // Prepare Java imports
         Set<String> qbeMetawidgetImports = this.qbeMetawidget.getImports();
         Set<String> rmEntityMetawidgetImports = this.rmEntityMetawidget.getImports();
         Set<String> metawidgetImports = CollectionUtils.newHashSet();
         metawidgetImports.addAll(qbeMetawidgetImports);
         metawidgetImports.addAll(rmEntityMetawidgetImports);
         metawidgetImports.remove(entity.getQualifiedName());
         context.put("metawidgetImports",
                  CollectionUtils.toString(metawidgetImports, ";\r\nimport ", true, false));

         // Create the Backing Bean for this entity
         JavaClass viewBean = JavaParser.parse(JavaClass.class, this.backingBeanTemplate.render(context));
         viewBean.setPackage(java.getBasePackage() + ".view");
         result.add(ScaffoldUtil.createOrOverwrite(this.prompt, java.getJavaResource(viewBean), viewBean.toString(),
                  overwrite));

         // Set new context for view generation
         context = getTemplateContext(targetDir, template);
         String beanName = StringUtils.decapitalize(viewBean.getName());
         context.put("beanName", beanName);
         context.put("ccEntity", ccEntity);
         context.put("entityName", StringUtils.uncamelCase(entity.getName()));
         setPrimaryKeyMetaData(context, entity);

         // Prepare entityMetawidget
         this.entityMetawidget.setValue(StaticFacesUtils.wrapExpression(beanName + "." + ccEntity));
         this.entityMetawidget.setPath(entity.getQualifiedName());
         this.entityMetawidget.setReadOnly(false);
         this.entityMetawidget.setStyle(null);

         // Generate create
         writeEntityMetawidget(context, this.createTemplateEntityMetawidgetIndent, this.createTemplateNamespaces);

         result.add(ScaffoldUtil.createOrOverwrite(this.prompt,
                  web.getWebResource(targetDir + "/" + ccEntity + "/create.xhtml"),
                  this.createTemplate.render(context),
                  overwrite));

         // Generate view
         this.entityMetawidget.setReadOnly(true);
         writeEntityMetawidget(context, this.viewTemplateEntityMetawidgetIndent, this.viewTemplateNamespaces);

         result.add(ScaffoldUtil.createOrOverwrite(this.prompt,
                  web.getWebResource(targetDir + "/" + ccEntity + "/view.xhtml"),
                  this.viewTemplate.render(context), overwrite));

         // Generate search
         this.searchMetawidget.setValue(StaticFacesUtils.wrapExpression(beanName + ".example"));
         this.searchMetawidget.setPath(entity.getQualifiedName());
         this.beanMetawidget.setValue(StaticFacesUtils.wrapExpression(beanName + ".pageItems"));
         this.beanMetawidget.setPath(viewBean.getQualifiedName() + "/pageItems");
         writeSearchAndBeanMetawidget(context, this.searchTemplateSearchMetawidgetIndent,
                  this.searchTemplateBeanMetawidgetIndent, this.searchTemplateNamespaces);

         result.add(ScaffoldUtil.createOrOverwrite(this.prompt,
                  web.getWebResource(targetDir + "/" + ccEntity + "/search.xhtml"),
                  this.searchTemplate.render(context), overwrite));

         // Generate navigation
         result.add(generateNavigation(targetDir, overwrite));

         // Need ViewUtils and forge.taglib.xml for forgeview:asList
         JavaClass viewUtils = JavaParser.parse(JavaClass.class, this.viewUtilsTemplate.render(context));
         viewUtils.setPackage(viewBean.getPackage());
         result.add(ScaffoldUtil.createOrOverwrite(this.prompt, java.getJavaResource(viewUtils), viewUtils.toString(),
                  true));

         context.put("viewPackage", viewBean.getPackage());
         result.add(ScaffoldUtil.createOrOverwrite(this.prompt,
                  web.getWebResource("WEB-INF/classes/META-INF/forge.taglib.xml"),
                  this.taglibTemplate.render(context), true));

         createInitializers(entity);
      }
      catch (Exception e)
      {
         throw new RuntimeException("Error generating default scaffolding: " + e.getMessage(), e);
      }
      return result;
   }

   @Override
   @SuppressWarnings("unchecked")
   public boolean install()
   {
      if(!isInstalled())
      {
         this.install.fire(new InstallFacets(WebResourceFacet.class, PersistenceFacet.class, CDIFacet.class,
                  FacesFacet.class));
      }
      return true;
   }

   @Override
   public boolean isInstalled()
   {
      if (this.project.hasFacet(WebResourceFacet.class) && this.project.hasFacet(PersistenceFacet.class)
               && this.project.hasFacet(CDIFacet.class) && this.project.hasFacet(FacesFacet.class))
      {
         // If the facet installation is in progress due to an InstallFacet event being fired, then the files would not
         // be available since ScaffoldProvider.setup would not have been executed, yet. We relax the facet installed
         // condition in such a case, to include only depdendencies being installed.
         Object isInstallInProgress = this.project.getAttribute(INSTALLING_SCAFFOLD);
         if (isInstallInProgress != null && isInstallInProgress.equals(Boolean.TRUE))
         {
            return true;
         }
         WebResourceFacet web = this.project.getFacet(WebResourceFacet.class);
         boolean areResourcesInstalled = web.getWebResource(ERROR_XHTML).exists() && web.getWebResource(ADD_PNG).exists()
                  && web.getWebResource(BOOTSTRAP_CSS).exists()
                  && web.getWebResource(FALSE_PNG).exists()
                  && web.getWebResource(FAVICON_ICO).exists()
                  && web.getWebResource(FORGE_LOGO_PNG).exists()
                  && web.getWebResource(FORGE_STYLE_CSS).exists()
                  && web.getWebResource(REMOVE_PNG).exists()
                  && web.getWebResource(SEARCH_PNG).exists()
                  && web.getWebResource(TRUE_PNG).exists()
                  && web.getWebResource(JBOSS_COMMUNITY_PNG).exists();
         return areResourcesInstalled;
      }
      return false;
   }

   @Override
   public List<Resource<?>> generateIndex(String targetDir, final Resource<?> template, final boolean overwrite)
   {
      List<Resource<?>> result = new ArrayList<Resource<?>>();
      WebResourceFacet web = this.project.getFacet(WebResourceFacet.class);

      this.project.getFacet(ServletFacet.class).getConfig().welcomeFile("/index.html");
      loadTemplates();

      generateTemplates(targetDir, overwrite);
      HashMap<Object, Object> context = getTemplateContext(targetDir, template);

      // Basic pages

      result.add(ScaffoldUtil.createOrOverwrite(this.prompt, web.getWebResource(targetDir + "/index.html"),
               this.indexWelcomeTemplate.render(context), overwrite));

      result.add(ScaffoldUtil.createOrOverwrite(this.prompt, web.getWebResource(targetDir + "/index.xhtml"),
               this.indexTemplate.render(context), overwrite));

      result.add(ScaffoldUtil.createOrOverwrite(this.prompt, web.getWebResource(ERROR_XHTML),
               this.errorTemplate.render(context), overwrite));

      // Static resources

      result.add(ScaffoldUtil.createOrOverwrite(this.prompt, web.getWebResource(ADD_PNG),
               getClass().getResourceAsStream("/scaffold/faces/add.png"), overwrite));

      result.add(ScaffoldUtil.createOrOverwrite(this.prompt, web.getWebResource(BOOTSTRAP_CSS),
               getClass().getResourceAsStream("/scaffold/faces/bootstrap.css"), overwrite));

      result.add(ScaffoldUtil.createOrOverwrite(this.prompt, web.getWebResource(FALSE_PNG),
               getClass().getResourceAsStream("/scaffold/faces/false.png"), overwrite));

      result.add(ScaffoldUtil.createOrOverwrite(this.prompt, web.getWebResource(FAVICON_ICO),
               getClass().getResourceAsStream("/scaffold/faces/favicon.ico"), overwrite));

      result.add(ScaffoldUtil.createOrOverwrite(this.prompt, web.getWebResource(FORGE_LOGO_PNG),
               getClass().getResourceAsStream("/scaffold/faces/forge-logo.png"), overwrite));

      result.add(ScaffoldUtil.createOrOverwrite(this.prompt, web.getWebResource(FORGE_STYLE_CSS),
               getClass().getResourceAsStream("/scaffold/faces/forge-style.css"), overwrite));

      result.add(ScaffoldUtil.createOrOverwrite(this.prompt, web.getWebResource(REMOVE_PNG),
               getClass().getResourceAsStream("/scaffold/faces/remove.png"), overwrite));

      result.add(ScaffoldUtil.createOrOverwrite(this.prompt, web.getWebResource(SEARCH_PNG),
               getClass().getResourceAsStream("/scaffold/faces/search.png"), overwrite));

      result.add(ScaffoldUtil.createOrOverwrite(this.prompt, web.getWebResource(TRUE_PNG),
               getClass().getResourceAsStream("/scaffold/faces/true.png"), overwrite));

      result.add(ScaffoldUtil.createOrOverwrite(this.prompt, web.getWebResource(JBOSS_COMMUNITY_PNG),
               getClass().getResourceAsStream("/scaffold/faces/jboss-community.png"), overwrite));

      return result;
   }

   @Override
   public List<Resource<?>> getGeneratedResources(String targetDir)
   {
      throw new RuntimeException("Not yet implemented!");
   }

   @Override
   public AccessStrategy getAccessStrategy()
   {
      return new FacesAccessStrategy(this.project);
   }

   @Override
   public TemplateStrategy getTemplateStrategy()
   {
      return new FacesTemplateStrategy(this.project);
   }

   @Override
   public List<Resource<?>> generateTemplates(String targetDir, final boolean overwrite)
   {
      List<Resource<?>> result = new ArrayList<Resource<?>>();

      try
      {
         WebResourceFacet web = this.project.getFacet(WebResourceFacet.class);

         result.add(ScaffoldUtil.createOrOverwrite(this.prompt,
                  web.getWebResource("/resources/scaffold/paginator.xhtml"),
                  getClass().getResourceAsStream("/scaffold/faces/paginator.xhtml"),
                  overwrite));

         result.add(generateNavigation(targetDir, overwrite));
      }
      catch (Exception e)
      {
         throw new RuntimeException("Error generating default templates", e);
      }

      return result;
   }

   //
   // Protected methods (nothing is private, to help subclassing)
   //
   protected void loadTemplates()
   {
      if (this.backingBeanTemplate == null)
      {
         this.backingBeanTemplate = this.compiler.compile(BACKING_BEAN_TEMPLATE);
         String template = Streams.toString(this.backingBeanTemplate.getSourceTemplateResource().getInputStream());
         this.backingBeanTemplateQbeMetawidgetIndent = parseIndent(template, "@{qbeMetawidget}");
         this.backingBeanTemplateRmEntityMetawidgetIndent = parseIndent(template, "@{rmEntityMetawidget}");
      }
      if (this.viewUtilsTemplate == null)
      {
         this.viewUtilsTemplate = this.compiler.compile(VIEW_UTILS_TEMPLATE);
      }
      if (this.taglibTemplate == null)
      {
         this.taglibTemplate = this.compiler.compile(TAGLIB_TEMPLATE);
      }
      if (this.viewTemplate == null)
      {
         this.viewTemplate = this.compiler.compile(VIEW_TEMPLATE);
         String template = Streams.toString(this.viewTemplate.getSourceTemplateResource().getInputStream());
         this.viewTemplateNamespaces = parseNamespaces(template);
         this.viewTemplateEntityMetawidgetIndent = parseIndent(template, "@{metawidget}");
      }
      if (this.createTemplate == null)
      {
         this.createTemplate = this.compiler.compile(CREATE_TEMPLATE);
         String template = Streams.toString(this.createTemplate.getSourceTemplateResource().getInputStream());
         this.createTemplateNamespaces = parseNamespaces(template);
         this.createTemplateEntityMetawidgetIndent = parseIndent(template, "@{metawidget}");
      }
      if (this.searchTemplate == null)
      {
         this.searchTemplate = this.compiler.compile(SEARCH_TEMPLATE);
         String template = Streams.toString(this.searchTemplate.getSourceTemplateResource().getInputStream());
         this.searchTemplateNamespaces = parseNamespaces(template);
         this.searchTemplateSearchMetawidgetIndent = parseIndent(template, "@{searchMetawidget}");
         this.searchTemplateBeanMetawidgetIndent = parseIndent(template, "@{beanMetawidget}");
      }
      if (this.navigationTemplate == null)
      {
         this.navigationTemplate = this.compiler.compile(NAVIGATION_TEMPLATE);
         String template = Streams.toString(this.navigationTemplate.getSourceTemplateResource().getInputStream());
         this.navigationTemplateIndent = parseIndent(template, "@{navigation}");
      }
      if (this.errorTemplate == null)
      {
         this.errorTemplate = this.compiler.compile(ERROR_TEMPLATE);
      }
      if (this.indexTemplate == null)
      {
         this.indexTemplate = this.compiler.compile(INDEX_TEMPLATE);
      }
      if (this.indexWelcomeTemplate == null)
      {
         this.indexWelcomeTemplate = this.compiler.compile(INDEX_WELCOME_TEMPLATE);
      }
   }

   protected void setupRichFaces()
   {
      if ((this.project.getFacet(DependencyFacet.class).hasEffectiveDependency(this.richfaces3UI)
               && this.project.getFacet(DependencyFacet.class).hasEffectiveDependency(this.richfaces3Impl))
               || (this.project.getFacet(DependencyFacet.class).hasEffectiveDependency(this.richfaces4UI)
               && this.project.getFacet(DependencyFacet.class).hasEffectiveDependency(this.richfaces4Impl)))
      {
         this.entityMetawidget
                  .setWidgetBuilder(insertRichFacesWidgetBuilder((CompositeWidgetBuilder<StaticWidget, StaticMetawidget>) this.entityMetawidget
                           .getWidgetBuilder()));

         this.searchMetawidget
                  .setWidgetBuilder(insertRichFacesWidgetBuilder((CompositeWidgetBuilder<StaticWidget, StaticMetawidget>) this.searchMetawidget
                           .getWidgetBuilder()));

         this.beanMetawidget
                  .setWidgetBuilder(insertRichFacesWidgetBuilder((CompositeWidgetBuilder<StaticWidget, StaticMetawidget>) this.beanMetawidget
                           .getWidgetBuilder()));
      }
   }

   /**
    * Locates a <code>ReadOnlyWidgetBuilder</code> in the list of WidgetBuilders, and inserts a
    * <code>RichFacesWidgetBuilder</code> after it (unless there's a <code>RichFacesWidgetBuilder</code> in there
    * already).
    */
   protected <W extends StaticWidget, M extends W> CompositeWidgetBuilder<W, M> insertRichFacesWidgetBuilder(
            final CompositeWidgetBuilder<W, M> compositeWidgetBuilder)
   {
      // Get the current WidgetBuilders...

      WidgetBuilder<W, M>[] existingWidgetBuilders = compositeWidgetBuilder.getWidgetBuilders();

      // ...find the ReadOnlyWidgetBuilder (if any)...

      int addAt = 0;

      for (int loop = 0; loop < existingWidgetBuilders.length; loop++)
      {
         // ...(abort if there's already a RichFacesWidgetBuilder)...

         // Use an Object loop variable here to avoid a nasty Java/Generics compiler bug
         Object widgetBuilder = existingWidgetBuilders[loop];
         if (widgetBuilder instanceof RichFacesWidgetBuilder)
         {
            return compositeWidgetBuilder;
         }

         if (widgetBuilder instanceof ReadOnlyWidgetBuilder)
         {
            addAt = loop + 1;
         }
      }

      // ...and insert our RichFacesWidgetBuilder just after it

      @SuppressWarnings("unchecked")
      WidgetBuilder<W, M>[] newWidgetBuilders = (WidgetBuilder<W, M>[]) ArrayUtils.addAt(existingWidgetBuilders, addAt,
               new RichFacesWidgetBuilder());

      return new CompositeWidgetBuilder<W, M>(
               new CompositeWidgetBuilderConfig<W, M>().setWidgetBuilders(newWidgetBuilders));
   }

   protected void createInitializers(final JavaClass entity) throws FacetNotFoundException, FileNotFoundException
   {
      boolean dirtyBit = false;
      for (Field<JavaClass> field : entity.getFields())
      {
         if (field.hasAnnotation(OneToOne.class))
         {
            Annotation<JavaClass> oneToOne = field.getAnnotation(OneToOne.class);
            if (oneToOne.getStringValue("mappedBy") == null && oneToOne.getStringValue("cascade") == null)
            {
               oneToOne.setEnumValue("cascade", CascadeType.ALL);
               dirtyBit = true;
            }
            String methodName = "new" + StringUtils.capitalize(field.getName());
            if (!entity.hasMethodSignature(methodName))
            {
               entity.addMethod().setName(methodName).setReturnTypeVoid().setPublic()
                        .setBody("this." + field.getName() + " = new " + field.getType() + "();");
               dirtyBit = true;
            }
         }
      }
      for (Method<JavaClass> method : entity.getMethods())
      {
         if (method.hasAnnotation(OneToOne.class))
         {
            Annotation<JavaClass> oneToOne = method.getAnnotation(OneToOne.class);
            if (oneToOne.getStringValue("mappedBy") == null && oneToOne.getStringValue("cascade") == null)
            {
               oneToOne.setEnumValue("cascade", CascadeType.ALL);
               dirtyBit = true;
            }
            String fieldName = StringUtils.camelCase(method.getName().substring(3));
            String methodName = "new" + StringUtils.capitalize(fieldName);
            if (!entity.hasMethodSignature(methodName))
            {
               entity.addMethod().setName(methodName).setReturnTypeVoid().setPublic()
                        .setBody("this." + fieldName + " = new " + method.getReturnType() + "();");
               dirtyBit = true;
            }
         }
      }
      if(dirtyBit)
      {
         this.project.getFacet(JavaSourceFacet.class).saveJavaSource(entity);
      }
   }

   protected HashMap<Object, Object> getTemplateContext(String targetDir, final Resource<?> template)
   {
      HashMap<Object, Object> context;
      context = new HashMap<Object, Object>();
      context.put("template", template);
      context.put("templateStrategy", getTemplateStrategy());
      context.put("targetDir", targetDir);
      return context;
   }

   protected void setupWebXML()
   {
      ServletFacet servlet = this.project.getFacet(ServletFacet.class);

      Node webXML = removeConflictingErrorPages(servlet);
      servlet.getConfigFile().setContents(XMLParser.toXMLInputStream(webXML));

      WebAppDescriptor servletConfig = servlet.getConfig();
      WebResourceFacet web = this.project.getFacet(WebResourceFacet.class);

      // (prefer /faces/error.xhtml)

      String errorLocation = getAccessStrategy().getWebPaths(web.getWebResource(ERROR_XHTML)).get(1);
      servletConfig.errorPage(404, errorLocation);
      servletConfig.errorPage(500, errorLocation);
      
      // Use the server timezone since we accept dates in that timezone, and it makes sense to display them in the same
      if (servletConfig.getContextParam("javax.faces.DATETIMECONVERTER_DEFAULT_TIMEZONE_IS_SYSTEM_TIMEZONE") == null)
      {
         servletConfig.contextParam("javax.faces.DATETIMECONVERTER_DEFAULT_TIMEZONE_IS_SYSTEM_TIMEZONE", "true");
      }

      servlet.saveConfig(servletConfig);
   }

   protected Node removeConflictingErrorPages(final ServletFacet servlet)
   {
      Node webXML = XMLParser.parse(servlet.getConfigFile().getResourceInputStream());
      Node root = webXML.getRoot();
      List<Node> errorPages = root.get("error-page");

      for (String code : Arrays.asList("404", "500"))
      {
         for (Node errorPage : errorPages)
         {
            if (code.equals(errorPage.getSingle("error-code").getText())
                     && this.prompt.promptBoolean("Your web.xml already contains an error page for " + code
                              + " status codes, replace it?"))
            {
               root.removeChild(errorPage);
            }
         }
      }
      return webXML;
   }

   /**
    * Generates the navigation menu based on scaffolded entities.
    */

   protected Resource<?> generateNavigation(final String targetDir, final boolean overwrite)
            throws IOException
   {
      WebResourceFacet web = this.project.getFacet(WebResourceFacet.class);
      HtmlTag unorderedList = new HtmlTag("ul");

      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;
         }
      };

      for (Resource<?> resource : web.getWebResource(targetDir + "/").listResources(filter))
      {
         HtmlOutcomeTargetLink outcomeTargetLink = new HtmlOutcomeTargetLink();
         outcomeTargetLink.putAttribute("outcome", targetDir + "/" + resource.getName() + "/search");
         outcomeTargetLink.setValue(StringUtils.uncamelCase(resource.getName()));

         HtmlTag listItem = new HtmlTag("li");
         listItem.getChildren().add(outcomeTargetLink);
         unorderedList.getChildren().add(listItem);
      }

      Writer writer = new IndentedWriter(new StringWriter(), this.navigationTemplateIndent);
      unorderedList.write(writer);

      Map<Object, Object> context = CollectionUtils.newHashMap();
      context.put("appName", StringUtils.uncamelCase(this.project.getProjectRoot().getName()));
      context.put("navigation", writer.toString().trim());
      context.put("targetDir", targetDir);

      if (this.navigationTemplate == null)
      {
         loadTemplates();
      }

      try
      {
         return ScaffoldUtil.createOrOverwrite(this.prompt, (FileResource<?>) getTemplateStrategy()
                  .getDefaultTemplate(),
                  this.navigationTemplate.render(context),
                  overwrite);
      }
      finally
      {
         writer.close();
      }
   }

   /**
    * Parses the given XML and determines what namespaces it already declares. These are later removed from the list of
    * namespaces that Metawidget introduces.
    */
   protected Map<String, String> parseNamespaces(final String template)
   {
      Map<String, String> namespaces = CollectionUtils.newHashMap();
      Document document = XmlUtils.documentFromString(template);
      Element element = document.getDocumentElement();
      NamedNodeMap attributes = element.getAttributes();

      for (int loop = 0, length = attributes.getLength(); loop < length; loop++)
      {
         org.w3c.dom.Node node = attributes.item(loop);
         String nodeName = node.getNodeName();
         int indexOf = nodeName.indexOf(XMLNS_PREFIX);

         if (indexOf == -1)
         {
            continue;
         }

         namespaces.put(nodeName.substring(indexOf + XMLNS_PREFIX.length()), node.getNodeValue());
      }

      return namespaces;
   }

   /**
    * Parses the given XML and determines the indent of the given String namespaces that Metawidget introduces.
    */
   protected int parseIndent(final String template, final String indentOf)
   {
      int indent = 0;
      int indexOf = template.indexOf(indentOf);

      while ((indexOf >= 0) && (template.charAt(indexOf) != '\n'))
      {
         if (template.charAt(indexOf) == '\t')
         {
            indent++;
         }

         indexOf--;
      }

      return indent;
   }

   /**
    * Writes the entity Metawidget and its namespaces into the given context.
    */
   protected void writeEntityMetawidget(final Map<Object, Object> context, final int entityMetawidgetIndent,
            final Map<String, String> existingNamespaces)
   {
      StringWriter stringWriter = new StringWriter();
      this.entityMetawidget.write(stringWriter, entityMetawidgetIndent);
      context.put("metawidget", stringWriter.toString().trim());

      Map<String, String> namespaces = this.entityMetawidget.getNamespaces();
      namespaces.keySet().removeAll(existingNamespaces.keySet());
      context.put("metawidgetNamespaces", namespacesToString(namespaces));
   }

   /**
    * Writes the search Metawidget, the bean Metawidget and their namespaces into the given context.
    */
   protected void writeSearchAndBeanMetawidget(final Map<Object, Object> context, final int searchMetawidgetIndent,
            final int beanMetawidgetIndent,
            final Map<String, String> existingNamespaces)
   {
      StringWriter stringWriter = new StringWriter();
      this.searchMetawidget.write(stringWriter, searchMetawidgetIndent);
      context.put("searchMetawidget", stringWriter.toString().trim());

      stringWriter = new StringWriter();
      this.beanMetawidget.write(stringWriter, beanMetawidgetIndent);
      context.put("beanMetawidget", stringWriter.toString().trim());

      Map<String, String> namespaces = this.searchMetawidget.getNamespaces();
      namespaces.putAll(this.beanMetawidget.getNamespaces());
      namespaces.keySet().removeAll(existingNamespaces.keySet());
      context.put("metawidgetNamespaces", namespacesToString(namespaces));
   }

   protected String namespacesToString(final Map<String, String> namespaces)
   {
      StringBuilder builder = new StringBuilder();

      for (Map.Entry<String, String> entry : namespaces.entrySet())
      {
         // At the start, break out of the current quote. Field must be in quotes so that we're valid XML

         builder.append("\"\r\n\txmlns:");
         builder.append(entry.getKey());
         builder.append("=\"");
         builder.append(entry.getValue());
      }

      return builder.toString();
   }

   private void setPrimaryKeyMetaData(Map<Object, Object> context, final JavaClass entity)
   {
      String pkName = "id";
      String pkType = "Long";
      String nullablePkType = "Long";
      for (Member<JavaClass, ?> m : entity.getMembers())
      {
         if (m.hasAnnotation(Id.class))
         {
            if (m instanceof Field)
            {
               Field<?> field = (Field<?>) m;
               pkName = field.getName();
               pkType = field.getType();
               nullablePkType = pkType;
               break;
            }

            Method<?> method = (Method<?>) m;
            pkName = method.getName().substring(3);
            if (method.getName().startsWith("get"))
            {
               pkType = method.getReturnType();
            }
            else
            {
               pkType = method.getParameters().get(0).getType();
            }
            nullablePkType = pkType;
            break;
         }
      }

      if ("int".equals(pkType))
      {
         nullablePkType = Integer.class.getSimpleName();
      }
      else if ("short".equals(pkType))
      {
         nullablePkType = Short.class.getSimpleName();
      }
      else if ("byte".equals(pkType))
      {
         nullablePkType = Byte.class.getSimpleName();
      }
      else if ("long".equals(pkType))
      {
         nullablePkType = Long.class.getSimpleName();
      }

      context.put("primaryKey", pkName);
      context.put("primaryKeyCC", StringUtils.capitalize(pkName));
      context.put("primaryKeyType", pkType);
      context.put("nullablePrimaryKeyType", nullablePkType);
   }
   
   private boolean isScaffoldable(JavaClass entity)
   {
      int setterCount = 0;
      for (Method<JavaClass> method : entity.getMethods())
      {
         // Exclude static methods

         if (method.isStatic())
         {
            continue;
         }

         // Get type

         List<Parameter<JavaClass>> parameters = method.getParameters();

         if (parameters.size() != 1)
         {
            continue;
         }

         // Get name

         String setterPropertyName = getSetterProperty(method);

         if (setterPropertyName == null)
         {
            continue;
         }

         Field<?> privateField = getPrivateField((FieldHolder<?>) entity, setterPropertyName);

         if (privateField != null)
         {
            // TODO Verify other aspects like whether the property is transient or has a generated value etc.
            setterCount++;
         }
      }
      if (setterCount > 0)
      {
         return true;
      }
      else
      {
         return false;
      }
   }
   
   /**
    * Returns the property name corresponding to the given 'setter' method.
    * Returns null if the supplied method is not a setter. 
    *
    * @param method a single-parametered method. May return non-void (ie. for Fluent interfaces)
    * @return the property name
    */
   private String getSetterProperty(final Method<?> method)
   {
      String methodName = method.getName();

      if (!methodName.startsWith(ClassUtils.JAVABEAN_SET_PREFIX))
      {
         return null;
      }

      String propertyName = methodName.substring(ClassUtils.JAVABEAN_SET_PREFIX.length());

      return StringUtils.decapitalize(propertyName);
   }
   
   /**
    * Gets the private field representing the given <code>propertyName</code> within the given class.
    *
    * @return the private Field for this propertyName, or null if no such field (should not throw NoSuchFieldException)
    */
   private Field<?> getPrivateField(final FieldHolder<?> fieldHolder, final String propertyName)
   {
      Field<?> field = fieldHolder.getField(propertyName);

      // FORGE-402: support fields starting with capital letter

      if (field == null && !Character.isUpperCase(propertyName.charAt( 0 )))
      {
         field = fieldHolder.getField(StringUtils.capitalize(propertyName));
      }

      return field;
   }
}
