js.wood.Project Maven / Gradle / Ivy
Show all versions of wood Show documentation
package js.wood;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import js.util.Files;
import js.util.Params;
import js.util.Strings;
/**
* Project global singleton. A project has a root directory; all {@link Path} instances are relative to this project root.
* Regarding build process, project reads component files under {@link Builder} control. For this, project scans source
* directories hierarchy, collects and caches meta data that is available for entire lifetime of the build process. Note that
* Project does not consider all sub-directories from project root; only source directories. This allows Project to be part of a
* larger project layout, like an Eclipse project.
*
* Current Project file system is depicted below. There are four source directories and one build target. There is also a
* project configuration XML file, see {@link ProjectConfig}. It is acceptable to share directories with master project, if any.
* For example lib
can hold Java archives. Anyway, master project files must not use extensions expected by this
* tool library and file name should obey syntax described by {@link FilePath}.
*
*
* / ; project root
* /build/site ; site build target directory
* /gen/ ; optional generated scripts, mostly HTTP-RMI stubs
* /lib/ ; third-party user interface components and script libraries
* /res/ ; application user interface resources
* | /asset ; project assets directory stores global variables and media files
* | /theme ; site styles for UI primitive elements and theme variables
* ~ ; application defined components
* /script/ ; application specific scripts structured in packages
* +-project.xml ; project configuration file
*
*
* Note that default build target directory is a sub-directory of master project build. This is to allow storing all build files
* in the same place. Anyway, site build directory is Project configurable, see site-dir
from
* {@link ProjectConfig}.
*
* Project instance is used in two different modes: build and preview. When working for build, project scans and cache files
* meta data; cache is valid for entire build process. On preview mode project does not use cache in order to display most
* updated content.
*
* @author Iulian Rotaru
* @version draft
*/
public final class Project {
// ------------------------------------------------------
// Project instance
/**
* Project name is used for internal representation. This value may be supplied by {@link ProjectConfig#getName(String)}
* with default to project directory name.
*/
private final String name;
private final String previewName;
/**
* Project display is for user interface. If this value is not provided by {@link ProjectConfig#getDisplay(String)} uses
* project name converted to title case.
*/
private final String display;
/**
* Project description. Uses project display if this value is not provided by {@link ProjectConfig#getDescription(String)}.
*/
private final String description;
/** Project directory. All project file are included here, no external references allowed. */
private final File projectDir;
private final DirPath resourcesDir;
/**
* Assets are variables and media files used in common by all components. Do not abuse it since it breaks component
* encapsulation. This directory is optional.
*/
private final DirPath assetsDir;
/**
* Contains site styles for UI primitive elements and theme variables. This directory content describe overall site design -
* for example flat design, and is usually imported. Theme directory is optional.
*/
private final DirPath themeDir;
/**
* Project configuration loaded from project.xml
file. By convention, configuration file should be stored
* project directory root.
*/
private final ProjectConfig config;
/** List of paths excluded from build process. Configurable per project, see {@link ProjectConfig#getExcludes()}. */
private final List excludes;
/**
* Project operator handler based on project selected naming strategy. Default naming strategy is
* {@link NamingStrategy#XMLNS}.
*/
private final OperatorsHandler operatorsHandler;
/** Strategy used for script dependencies resolving for inclusion into build and preview. */
private final ScriptDependencyStrategy scriptDependencyStrategy;
/** Site build directory, usually part of master project build. */
private File siteDir;
/**
* Construct not initialized project instance. Initialize project instance state. Load project configuration and create
* directories, if missing.
*
* @param projectPath path to project root directory.
* @throws IllegalArgumentException if project root does not designate an existing directory.
*/
public Project(String projectPath) throws IllegalArgumentException {
this.projectDir = new File(projectPath);
Params.isDirectory(this.projectDir, "Project directory");
this.config = new ProjectConfig(this);
this.resourcesDir = new DirPath(this, CT.RESOURCE_DIR);
this.assetsDir = new DirPath(this, CT.ASSETS_DIR);
this.themeDir = new DirPath(this, CT.THEME_DIR);
this.siteDir = new File(projectPath, config.getSiteDir(CT.DEF_SITE_DIR));
if (!this.siteDir.exists()) {
this.siteDir.mkdir();
}
this.name = config.getName(this.projectDir.getName());
this.previewName = this.name.isEmpty() ? "preview" : this.name + "-preview";
this.display = config.getDisplay(Strings.toTitleCase(this.name));
this.description = config.getDescription(this.display);
this.excludes = config.getExcludes();
switch (this.config.getNamingStrategy()) {
case XMLNS:
operatorsHandler = new XmlnsOperatorsHandler();
break;
case DATA_ATTR:
operatorsHandler = new DataAttrOperatorsHandler();
break;
case ATTR:
operatorsHandler = new AttOperatorsHandler();
break;
default:
operatorsHandler = null;
}
scriptDependencyStrategy = this.config.getScriptDependencyStrategy();
}
/**
* Set site build directory. This method is designed for builder instance customization, see
* {@link Builder#setSiteDir(String).
*
* @param siteDir site build directory, relative to project root.
* @see siteDir
*/
public void setSiteDir(File siteDir) {
this.siteDir = siteDir;
}
/**
* Get project name for internal use.
*
* @return project name.
* @see #name
*/
public String getName() {
return name;
}
/**
* Get project name for preview servlet.
*
* @return project name used for preview.
* @see #previewName
*/
public String getPreviewName() {
return previewName;
}
/**
* Return project name usable on user interfaces.
*
* @return project display.
* @see #display
*/
public String getDisplay() {
return display;
}
/**
* Get project description.
*
* @return project description.
* @see #description
*/
public String getDescription() {
return description;
}
/**
* Get this project file system root. Project root directory contains all project files including Java code base and project
* user interface resources.
*
* @return project root directory.
* @see #projectDir
*/
public File getProjectDir() {
return projectDir;
}
public DirPath getResourcesDir() {
return resourcesDir;
}
/**
* Get project assets directory that contains variables and media files used in common by all components. Returned directory
* is optional and is caller responsibility to ensure it exists before using it.
*
* @return project assets directory.
* @see #assetsDir
*/
public DirPath getAssetsDir() {
return assetsDir;
}
/**
* Get site theme directory that contains styles for UI primitive elements and theme variables. Returned directory is
* optional and is caller responsibility to ensure it exists before using it.
*
* @return site theme directory.
* @see #themeDir
*/
public DirPath getThemeDir() {
return themeDir;
}
/**
* Get site build directory.
*
* @return site build directory.
* @see #siteDir
*/
public File getSiteDir() {
return siteDir;
}
/**
* Get the path, relative to project root, of the site build directory. Returned value is guaranteed to have trailing file
* separator.
*
* @return site build path.
*/
public String getSitePath() {
return Files.getRelativePath(projectDir, siteDir, true) + Path.SEPARATOR;
}
/**
* Get project configuration loaded from configuration file project.xml
.
*
* @return project configuration.
* @see #config
*/
public ProjectConfig getConfig() {
return config;
}
/**
* Get project favicon.
*
* @return project favicon.
*/
public FilePath getFavicon() {
return assetsDir.getFilePath(CT.FAVICON);
}
/**
* Get project supported locale settings as configured by project descriptor. Returned list is immutable.
*
* @return project locale.
* @see ProjectConfig#getLocales()
*/
public List getLocales() {
return config.getLocales();
}
/**
* Return true if this project has support for multiple locale settings.
*
* @return true if project is multi-locale.
*/
public boolean isMultiLocale() {
return config.getLocales().size() > 1;
}
public Locale getDefaultLocale() {
return config.getDefaultLocale();
}
/**
* Test if path is excluded from building process. If path
is a file test also if its parent is excluded.
*
* @param path path, file or directory.
* @return true if path is excluded from building process.
* @see #excludes
*/
public boolean isExcluded(Path path) {
if (path instanceof FilePath) {
FilePath filePath = (FilePath) path;
return excludes.contains(filePath) || excludes.contains(filePath.getDirPath());
}
return excludes.contains(path);
}
/**
* Create file path instance for a project file.
*
* @param path path value.
* @return file path instance.
*/
public FilePath getFile(String path) {
return new FilePath(this, path);
}
// ------------------------------------------------------
// Build files scanner and cache
/**
* Project layout files are HTM files describing user interface elements. By convention layout base name is the same as
* component directory name.
*/
private Set layouts = new HashSet<>();
/**
* Style files stored on project theme directory. These files contains global styles regarding UI primitive elements.
*/
private List themeStyles = new ArrayList<>();
/**
* Discovered project script files identified by related file paths. Includes all scripts from scripts source directory,
* library and generated scripts. Note that it does not include scripts from UI resources source directory since current
* version of WOOD does not support.
*/
private Map scripts = new HashMap<>();
/** Script file where certain script class is defined. */
private Map classScripts = new HashMap<>();
/** Cache for resource variables. */
private Map variables = new HashMap<>();
/**
* Scan for build files and cache resulted meta data. This method is specifically designed for {@link Builder} class;
* preview process does not use cache.
*/
public void scanBuildFiles() {
FilesScanner scanner = new FilesScanner();
scanner.scan(new DirPath(this, CT.RESOURCE_DIR));
scanner.scan(new DirPath(this, CT.SCRIPT_DIR));
scanner.scan(new DirPath(this, CT.LIBRARY_DIR));
scanner.scan(new DirPath(this, CT.GENERATED_DIR));
// scan for dependencies after all script files loaded, but only if discovery is enabled
if (scriptDependencyStrategy == ScriptDependencyStrategy.DISCOVERY) {
for (ScriptFile scriptFile : scripts.values()) {
scriptFile.scanDependencies(classScripts);
}
}
}
/**
* Project files scanner.
*
* @author Iulian Rotaru
* @since 1.0
*/
private class FilesScanner {
/** Specialized file handlers. */
private FilesHandler[] handlers = new FilesHandler[] { new LayoutsScanner(), new ScriptsScanner(), new ThemeStylesScanner(), new VariablesScanner() };
/**
* Scan given directory recursively till no more sub-directories.
*
* @param dir directory to scan.
*/
public void scan(DirPath dir) {
dir.files(new FilesHandler() {
@Override
public void onDirectory(DirPath dir) throws Exception {
for (FilesHandler handler : handlers) {
handler.onDirectory(dir);
}
scan(dir);
}
@Override
public void onFile(FilePath file) throws Exception {
for (FilesHandler handler : handlers) {
handler.onFile(file);
}
}
});
}
}
/**
* Scanner for layout files. Layouts are HTM files describing UI elements.
*
* @author Iulian Rotaru
* @since 1.0
*/
private class LayoutsScanner extends FilesHandler {
/**
* Add layout files to {@link Project#layouts} cache. By convention layout base name is the same as component name; also
* file should be layout, see {@link FilePath#isLayout()}.
*/
@Override
public void onFile(FilePath file) throws Exception {
if (file.isLayout() && file.isBaseName(file.getDirPath().getName()) && !file.isExcluded()) {
layouts.add(new LayoutFile(file));
}
}
}
/**
* Look for script files into all source directories less UI resources.
*
* @author Iulian Rotaru
*/
private class ScriptsScanner extends FilesHandler {
/**
* Add meta data related to script file to project scripts meta cache. All script files are accepted less those from UI
* resources source directory.
*/
@Override
public void onFile(FilePath file) throws Exception {
if (file.isScript() && !file.getDirPath().isResources() && !file.isExcluded()) {
ScriptFile scriptFile = new ScriptFile(Project.this, file);
scripts.put(file, scriptFile);
for (String scriptClass : scriptFile.getDefinedClasses()) {
classScripts.put(scriptClass, scriptFile);
}
}
}
}
/**
* Scanner for style files stored into theme directory. This scanner simply add site styles to {@link #themeStyles} list but
* takes care to include {@link CT#RESET_CSS} and {@link CT#FX_CSS} first, in mentioned order. After scanning completion
* {@link Project#themeStyles} contains style files in proper order for inclusion in page document.
*
* @author Iulian Rotaru
*/
private class ThemeStylesScanner extends FilesHandler {
/**
* True if {@link CT#RESET_CSS} file was processed. Help in deciding where {@link CT#FX_CSS} has to be inserted into
* {@link Project#themeStyles} list.
*/
private boolean resetProcessed;
/**
* Add style files residing in site styles directory to {@link Project#themeStyles} cache.
*/
@Override
public void onFile(FilePath file) throws Exception {
if (!file.getDirPath().isTheme()) {
return;
}
if (CT.RESET_CSS.equals(file.getName())) {
resetProcessed = true;
themeStyles.add(0, file);
return;
}
if (CT.FX_CSS.equals(file.getName())) {
themeStyles.add(resetProcessed ? 1 : 0, file);
return;
}
// do not include style variants since are included by preview servlet within media query sections
if (!file.hasVariants() && file.isStyle()) {
themeStyles.add(file);
}
}
}
/**
* Scanner for project variables. Variables are defined per directory and load values from all child XML files.
*
* @author Iulian Rotaru
*/
private class VariablesScanner extends FilesHandler {
/** Project assets variables. */
private Variables assetVariables;
/** Theme variables. */
private Variables themeVariables;
public VariablesScanner() {
assetVariables = new Variables(Project.this);
themeVariables = new Variables(Project.this);
// add assets directory to theme variables
// theme should be able to use global variables from assets
themeVariables.setAssetVariables(assetVariables);
variables.put(assetsDir, assetVariables);
variables.put(themeDir, themeVariables);
}
/**
* Create empty variables instance and map to directory. Project generated files and excluded directories are skipped.
*
* @param dir current processed directory.
* @see Project#excludes
*/
@Override
public void onDirectory(DirPath dir) throws Exception {
if (variables.containsKey(dir)) {
// test if variables cache already contains directory key in order to avoid overwrite
return;
}
if (dir.isGenerated() || dir.isExcluded()) {
// do not scan generated sources and excluded directories
return;
}
if (dir.isAssets() || dir.isTheme()) {
// asset and theme variables are created at this scanner instance creation
return;
}
Variables dirVariables = new Variables(Project.this);
dirVariables.setAssetVariables(assetVariables);
dirVariables.setThemeVariables(themeVariables);
variables.put(dir, dirVariables);
}
/**
* Load variable values from variables definition file to variables instance mapped to directory.
*
* @param file current processed file.
*/
@Override
public void onFile(FilePath file) throws Exception {
if (file.isVariables() && !file.isExcluded()) {
variables.get(file.getDirPath()).load(file);
}
}
}
/**
* Get project layout files, in no particular order. Returned collection is not modifiable.
*
* @return project layout files.
* @see #layouts
*/
public Set getLayouts() {
return Collections.unmodifiableSet(layouts);
}
/**
* Get project variables mapped to parent directories. Returned map is not modifiable. Every variables instance from map has
* a reference to project asset variables, used when variables miss a reference value.
*
* @return project variables.
* @see #variables
*/
public Map getVariables() {
return Collections.unmodifiableMap(variables);
}
/**
* Get style files declared into project theme directory. Returned set is not changeable.
*
* @return site styles.
* @see #themeStyles
*/
public List getThemeStyles() {
return Collections.unmodifiableList(themeStyles);
}
/**
* Create script file for for given preview file path and scan its dependencies. Returns created script file or null if
* given preview file does not exist.
*
* @param previewFile preview file path.
* @return preview script file or null.
*/
public ScriptFile getPreviewScript(FilePath previewFile) {
if (!previewFile.exists()) {
return null;
}
ScriptFile previewScript = new ScriptFile(this, previewFile);
// scan script dependencies only if discovery is enabled
if (scriptDependencyStrategy == ScriptDependencyStrategy.DISCOVERY) {
previewScript.scanDependencies(classScripts);
}
return previewScript;
}
/**
* Get script files defining certain script classes. It is a bug condition if is not possible to found script file for a
* particular script class.
*
* @param layoutFile layout file using script classes,
* @param scriptClasses script classes.
* @return script files, in no particular order.
* @throws WoodException if script file not found.
*/
public Collection getScriptFiles(Collection scriptClasses) {
Set scriptFiles = new HashSet();
for (String scriptClass : scriptClasses) {
ScriptFile scriptFile = classScripts.get(scriptClass);
if (scriptFile == null) {
throw new WoodException("Broken script reference. No script file found for class |%s|.", scriptClass);
}
scriptFiles.add(scriptFile);
}
return scriptFiles;
}
/**
* Test if a script class is indeed defined into a script file. This predicate is used when scanning layout files to ensure
* that declared script classes are valid.
*
* @param scriptClass script qualified class name.
* @return true if script class is indeed defined into a script file.
*/
public boolean scriptFileExists(String scriptClass) {
return classScripts.containsKey(scriptClass);
}
// ------------------------------------------------------
// Media files
/**
* Get project media file referenced from given source file. This method tries to locate media file into source path parent
* and assets directories, in this order. When search for media file only base name and language variant is considered, that
* is, no extension.
*
* There is no attempt to test reference type against actual file content or matching against file extension. It is
* developer responsibility to ensure reference points to proper type.
*
* Returns null if media file is not found.
*
* @param language language variant, possible null,
* @param reference media resource reference,
* @param source source file using media resource.
* @return media file or null.
*/
public FilePath getMediaFile(Locale locale, Reference reference, FilePath source) {
DirPath dir = source.getDirPath();
if (reference.hasPath()) {
dir = dir.getSubdirPath(reference.getPath());
}
FilePath file = mediaFile(dir, reference.getName(), locale);
if (file == null) {
dir = assetsDir;
if (reference.hasPath()) {
dir = dir.getSubdirPath(reference.getPath());
}
file = mediaFile(dir, reference.getName(), locale);
}
return file;
}
/**
* Scan directory for media files matching base name and language variant. This helper method is used by
* {@link #getMediaFile(String, Reference, FilePath)}. Try to locate file matching both base name and language; extension is
* not considered. If not found try to return base variant, that is, file that match only base name and has no language. If
* still not found returns null.
*
* @param dir directory to scan for media files,
* @param basename media file base name,
* @param language language variant, possible null.
* @return media file or null.
*/
public static FilePath mediaFile(DirPath dir, String basename, Locale locale) {
for (FilePath file : dir.files()) {
if (file.isMedia() && file.isBaseName(basename) && file.getVariants().hasLocale(locale)) {
return file;
}
}
for (FilePath file : dir.files()) {
if (file.isMedia() && file.isBaseName(basename)) {
return file;
}
}
return null;
}
// ------------------------------------------------------
// Preview support
/**
* Scan project for script files and return the discovered ones. This method is designed for preview process and does not
* use cache. It scans script, library and generated source directories. Delegates {@link #scanScriptFiles(DirPath)} for
* recursive directories scanning.
*
* @return project script files.
*/
public Collection previewScriptFiles() {
scripts.clear();
classScripts.clear();
// script files initialization is a two stages process:
// 1. scan for script files and initialize file and class scripts maps
// 2. determine each script file dependencies
scanScriptFiles(new DirPath(this, CT.SCRIPT_DIR));
scanScriptFiles(new DirPath(this, CT.LIBRARY_DIR));
scanScriptFiles(new DirPath(this, CT.GENERATED_DIR));
// scan script files dependencies only if discovery is enabled
if (scriptDependencyStrategy == ScriptDependencyStrategy.DISCOVERY) {
for (ScriptFile scriptFile : scripts.values()) {
scriptFile.scanDependencies(classScripts);
}
}
return Collections.unmodifiableCollection(scripts.values());
}
/**
* Scan directory and all its descendants for script files and caches the results. This method is invoked recursively on
* directories tree; after completion {@link #scripts} and {@link #classScripts} fields are updated accordingly.
*
* @param dir base directory to scan for script files.
*/
private void scanScriptFiles(DirPath dir) {
// in order to reuse script files scanning logic create an instance of ScriptsScanner class
// and delegate it from FilesHandler#onFile()
final ScriptsScanner scriptsScanner = new ScriptsScanner();
dir.files(new FilesHandler() {
@Override
public void onDirectory(DirPath dir) throws Exception {
scanScriptFiles(dir);
}
@Override
public void onFile(FilePath file) throws Exception {
scriptsScanner.onFile(file);
}
});
}
/**
* Scan theme directory for style files. This method is designed for preview process and does not use cache.
*
* @return site style files.
*/
public List previewThemeStyles() {
themeStyles.clear();
DirPath dir = new DirPath(this, CT.THEME_DIR);
dir.files(new ThemeStylesScanner());
return themeStyles;
}
/**
* Get operator handler as configured by selected naming strategy.
*
* @return operator handler for project naming convention.
*/
public OperatorsHandler getOperatorsHandler() {
return operatorsHandler;
}
/**
* Determine if project uses a naming convention that requires XML name space.
*
* @return true if project naming convention requires XML name space.
*/
public boolean hasNamespace() {
return operatorsHandler instanceof XmlnsOperatorsHandler;
}
public ScriptDependencyStrategy getScriptDependencyStrategy() {
return scriptDependencyStrategy;
}
private Map attributes = new HashMap<>();
public void setAttribute(String name, Object value) {
attributes.put(name, value);
}
public Object getAttribute(String name) {
return attributes.get(name);
}
}