Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* Copyright 2014 Attila Szegedi, Daniel Dekany, Jonathan Revusky
*
* 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 fmpp;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TimeZone;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import fmpp.setting.Settings;
import fmpp.util.BorderedReader;
import fmpp.util.BugException;
import fmpp.util.ExceptionCC;
import fmpp.util.FileUtil;
import fmpp.util.InstallationException;
import fmpp.util.MiscUtil;
import fmpp.util.StringUtil;
import freemarker.cache.TemplateConfigurationFactory;
import freemarker.cache.TemplateConfigurationFactoryException;
import freemarker.core.HTMLOutputFormat;
import freemarker.core.OutputFormat;
import freemarker.core.RTFOutputFormat;
import freemarker.core.TemplateConfiguration;
import freemarker.core.UndefinedOutputFormat;
import freemarker.core.UnregisteredOutputFormatException;
import freemarker.core.XHTMLOutputFormat;
import freemarker.core.XMLOutputFormat;
import freemarker.ext.beans.BeansWrapper;
import freemarker.ext.beans.BeansWrapperBuilder;
import freemarker.template.Configuration;
import freemarker.template.DefaultObjectWrapper;
import freemarker.template.DefaultObjectWrapperBuilder;
import freemarker.template.ObjectWrapper;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import freemarker.template.TemplateExceptionHandler;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateNodeModel;
import freemarker.template.Version;
import freemarker.template.utility.NullArgumentException;
/**
* The bare-bone, low-level preprocessor engine. Since FMPP 0.9.0 you should
* rather use a {@link fmpp.setting.Settings} object instead of directly using
* this class.
*
*
{@link Engine fmpp.Engine} vs {@link fmpp.setting.Settings}:
* The design of the {@link Engine} API is driven by the internal
* architecture of FMPP. It doesn't consider front-ends, doesn't know
* configuration files or similar high-level concepts. {@link Settings}
* wraps the {@link Engine} object, and implements end-user (front-end) centric
* concepts, as the settings and configuration files described in the FMPP
* Manual. For a programmer, the API of {@link Engine} is more straightforward
* than the API of {@code Settings} object. But {@code Settings} is better
* if you want FMPP behave similarly as described in the FMPP Manual from the
* viewpoint of end-user, or if you need some of its extra features, like
* configuration files.
*
*
Engine parameters:
* {@link Engine} parameters are very similar to "settings" discussed in the
* FMPP Manual. You will usually find trivial one-to-one correspondence between
* settings and {@link Engine} parameters, but not always, as {@link Settings} is
* a higher level API that adds some new concepts.
* The value of {@link Engine} parameters can't be set while a processing session is
* executing; attempting that will cause {@link java.lang.IllegalStateException}.
* Thus, for example, you can't change an {@link Engine} parameter from an executing
* template. Also, you should not change the objects stored as "data" (i.e. the
* variables that are visible for all templates) while the processing session is
* executing, even though it's not prevented technically.
*
*
Life-cycle: The same {@link Engine} object can be used for multiple
* processing sessions. However, the typical usage is that it's used
* only for a single processing session. The state of the engine object possibly
* changes during sessions because of the engine attributes (see
* {@link #setAttribute(String, Object)}), and because long-lived objects as local
* data builders and progress listeners can maintain state through multiple sessions.
* These objects should behave so that the output of a session is not influenced
* by earlier sessions.
*/
public class Engine {
/** Processing mode: N/A */
public static final int PMODE_NONE = 0;
/** Processing mode: Execute the file as template */
public static final int PMODE_EXECUTE = 1;
/** Processing mode: Copy the file as-is (binary copy). */
public static final int PMODE_COPY = 2;
/** Processing mode: Ignore the file. */
public static final int PMODE_IGNORE = 3;
/** Processing mode: Render XML with an FTL template. */
public static final int PMODE_RENDER_XML = 4;
/** Used with the "skipUnchnaged" engine parameter: never skip files */
public static final int SKIP_NONE = 0;
/**
* Used with the "skipUnchanged" engine parameter: skip unchanged static
* files
*/
public static final int SKIP_STATIC = 1;
/**
* Used with the "skipUnchanged" engine parameter: skip all unchanged
* files
*/
public static final int SKIP_ALL = 2;
/**
* A commonly used reserved parameter value: {@code "source"}.
*/
public static final String PARAMETER_VALUE_SOURCE = "source";
/**
* A commonly used reserved parameter value: {@code "source"}.
*/
public static final String PARAMETER_VALUE_OUTPUT = "output";
/**
* A commonly used reserved parameter value: {@code "host"}.
*/
public static final String PARAMETER_VALUE_HOST = "host";
/**
* Used as the value of the "xmlEngine" engine parameter: keep the current
* JVM level setting.
*/
public static final String XPATH_ENGINE_DONT_SET = "dontSet";
/**
* Used as the value of the "xmlEngine" engine parameter: Let FreeMarker
* choose.
*/
public static final String XPATH_ENGINE_DEFAULT = "default";
/**
* Used as the value of the "xmlEngine" engine parameter: Force the usage
* of Xalan.
*/
public static final String XPATH_ENGINE_XALAN = "xalan";
/**
* Used as the value of the "xmlEngine" engine parameter: Force the usage
* of Jaxen.
*/
public static final String XPATH_ENGINE_JAXEN = "jaxen";
public static final Version VERSION_0_9_15 = new Version(0, 9, 15);
public static final Version VERSION_0_9_16 = new Version(0, 9, 16);
/**
* The default value of the {@code recommendDefaults} setting, when {@code null} is passed for it to the
* {@link Engine} constructor. This was exposed as sometimes you need this information earlier than calling the
* {@link Engine} constructor.
*
* @since 0.9.16
*/
public static final Version DEFAULT_RECOMMENDED_DEFAULTS = VERSION_0_9_15;
private static final String IGNOREDIR_FILE = "ignoredir.fmpp";
private static final String CREATEDIR_FILE = "createdir.fmpp";
private static final Set STATIC_FILE_EXTS_V1;
private static final Set STATIC_FILE_EXTS_V2;
static {
STATIC_FILE_EXTS_V1 = new HashSet();
STATIC_FILE_EXTS_V1.addAll(Arrays.asList(
"jpg", "jpeg", "gif", "png", "swf", "bmp", "pcx", "tga", "tiff", "ico",
"zip", "gz", "tgz", "jar", "ace", "bz", "bz2", "tar", "arj",
"rar", "lha", "cab", "lzh", "taz", "tz", "arc",
"exe", "com", "msi", "class", "dll",
"doc", "xls", "pdf", "ps", "chm",
"avi", "wav", "mp3", "mpeg", "mpg", "wma", "mov", "fli"));
STATIC_FILE_EXTS_V2 = new HashSet(STATIC_FILE_EXTS_V1);
STATIC_FILE_EXTS_V2.addAll(Arrays.asList(
"webp", "svgz", "tif",
"7z", "xz", "txz", "tbz2", "tb2", "z",
"deb", "pkg", "rpm", "apk",
"iso", "bin", "dmg", "vcd",
"sys",
"docx", "dotx", "docm", "dot", "odt", "ott", "oth", "odm",
"xlsx", "xlsm", "xltx", "xltm", "xlw", "xlt", "ods", "ots",
"ppt", "pps", "pot", "pptx", "pptm", "potx", "potm", "odp", "odg", "otp",
"odg", "otg",
"mkv", "mp4", "m4v", "m4a", "webm", "mpa", "cda", "aif", "h264", "wma", "wmv", "3gp", "3g2",
"ogg", "oga", "mogg", "acc", "flac", "aiff",
"flv", "swf",
"fnt", "ttf", "otf", "woff", "woff2", "eot",
"der"));
}
private static Version cachedVersion;
private static String cachedBuildInfo;
// Settings
private final Version recommendedDefaults;
private File srcRoot, outRoot, dataRoot;
private boolean dontTraverseDirs;
private Map> freemarkerLinks = new HashMap>();
private boolean stopOnError = true;
private Map data = new HashMap();
private LayeredChooser localDataBuilders = new LayeredChooser();
private TemplateDataModelBuilder tdmBuilder;
private String outputEncoding = PARAMETER_VALUE_SOURCE;
private String urlEscapingCharset = PARAMETER_VALUE_OUTPUT;
private boolean mapCommonExtensionsToOutputFormats;
private List outputFormatChoosers = new ArrayList();
private List pModeChoosers = new ArrayList();
private LayeredChooser headerChoosers = new LayeredChooser();
private LayeredChooser footerChoosers = new LayeredChooser();
private List turnChoosers = new ArrayList();
private boolean csPathCmp = false;
private boolean expertMode = false;
private List removeExtensions = new ArrayList();
private List removePostfixes = new ArrayList();
private List replaceExtensions = new ArrayList();
private boolean removeFreemarkerExtensions;
private int skipUnchanged;
private boolean alwaysCrateDirs = false;
private boolean ignoreCvsFiles = true;
private boolean ignoreSvnFiles = true;
private boolean ignoreTemporaryFiles = true;
private String xpathEngine = XPATH_ENGINE_DONT_SET;
private Object xmlEntityResolver;
private boolean validateXml = false;
private List xmlRendCfgCntrs = new ArrayList();
// Misc
private Configuration fmCfg;
private MultiProgressListener progListeners = new MultiProgressListener();
private TemplateEnvironment templateEnv;
private int maxTurn, currentTurn;
private Map attributes = new HashMap();
private Boolean chachedXmlSupportAvailable;
private boolean parametersLocked;
// Session state
private Map ignoredDirCache = new HashMap();
private Set processedFiles = new HashSet();
/**
* Same as {@link #Engine(Version) Engine((Version) null)}.
*
* @deprecated Use {@link #Engine(Version)} instead.
*/
public Engine() {
this((Version) null);
}
/**
* Same as {@link #Engine(Version, Version, BeansWrapper) Engine(recommendedDefaults, null, null)}.
*/
public Engine(Version recommendedDefaults) {
this(recommendedDefaults, null, null);
}
/**
* Same as {@link #Engine(Version, Version, BeansWrapper) Engine(null, objectWrapper, null)}.
*
* @deprecated Use {@link #Engine(Version, Version, BeansWrapper)} instead.
*/
public Engine(BeansWrapper objectWrapper) {
this(objectWrapper, null);
}
/**
* Same as {@link #Engine(Version, Version, BeansWrapper) Engine(null, objectWrapper, fmIncompImprovements)}.
*
* @deprecated Use {@link #Engine(Version, Version, BeansWrapper)} instead.
*/
public Engine(BeansWrapper objectWrapper, Version freemarkerIncompatibleImprovements) {
this(null, freemarkerIncompatibleImprovements, objectWrapper);
}
/**
* Creates a new FMPP engine instance. Use the setter methods (as {@code setProgressListener}) to configure the new
* instance.
*
* @param recommendedDefaults
* Instructs the engine to use the setting value defaults recommended as of the specified FMPP version.
* When you start a new project, set this to the current FMPP version. In older projects changing this
* setting can break things (check what changes below). If {@code null}, then it defaults to the lowest
* allowed value, 0.9.15. (That's the lowest allowed because this setting was added in 0.9.16.)
*
The defaults change as follows:
*
*
*
0.9.15: This is the baseline (and the default)
*
0.9.16: The following defaults change (compared to 0.9.15):
*
*
{@code freemarkerIncompatibleImprovements} to 2.3.28, thus, among many things, templates
* with {@code ftlh} and {@code ftlx} file extensions will use {@code HTML} and {@code XML}
* auto-escaping respectively.
*
{@link #setMapCommonExtensionsToOutputFormats(boolean) mapCommonExtensionsToOutputFormats}
* to {@code true}, thus, templates with common file extensions like {@code html},
* {@code xml} etc. will have auto-escaping.
*
{@link #setRemoveFreemarkerExtensions(boolean) removeFreemarkerExtensions} to
* {@code true}, thus, the {@code ftl}, {@code ftlh}, and {@code ftlx} file extensions are
* automatically removed from the output file name.
*
The list of file extensions that are treated as binary files is extended (see them under
* "Settings" / "Processing mode choosing" in the FMPP Manual)
*
{@code objectWrapper} to a {@link freemarker.template.DefaultObjectWrapper}, if
* {@code freemarkerIncompatibleImprovements} is at least 2.3.21} There are more details,
* but see that at the {@code objectWrapper} parameter.
*
*
*
*
* @param freemarkerIncompatibleImprovements
* Sets the "incompatible improvements" version of FreeMarker. You should set this to the current
* FreeMarker version in new projects. See {@link Configuration#Configuration(Version)} for details.
* If this is {@code null} and the {@code recommendedDefaults} argument is 0.9.16, then
* "incompatible improvements" defaults to 2.3.28. If this is {@code null} and
* {@code recommendedDefaults} is 0.9.15 (the lowest possible value) then the default is chosen by
* FreeMarker (to 2.3.0 for maximum backward compatibility, at least currently).
* @param objectWrapper
* The FreeMarker {@link ObjectWrapper} that this instance will use. Just use {@code null} if you don't
* know what's this. When this parameter is {@code null}, FMPP chooses the default, considering
* FreeMarker best practices and backward compatibility concerns. So it's somewhat complex, and depends
* on both the {@code recommendedDefaults} and the {@code fmIncompImprovements} arguments.
* If {@code recommendedDefaults} is at least 0.9.16, and {@code fmIncompImprovements} is either
* {@code null} or at least 2.3.22, then FMPP creates a {@link DefaultObjectWrapper} with its
* {@code incompatibleImprovements} setting set to FreeMarker {@code incompatibleImprovements},
* its {@code forceLegacyNonListCollections} setting set to {@code false}, its
* {@code iterableSupport} setting to {@code true}, and its {@code treatDefaultMethodsAsBeanMembers}
* setting set to {@code true}.
* Otherwise, FMPP creates a {@code BeansWrapper} (not a {@link DefaultObjectWrapper}) with
* its {@code simpleMapWrapper} setting set to {@code true}, and also, if the
* FreeMarker {@code incompatibleImprovements} will be at least {@code 2.3.21}, it's created using
* {@link BeansWrapperBuilder} instead of {@code new BeansWrapper()}, which means that that the resulting
* {@link BeansWrapper} will be a shared singleton with read-only settings.
*
* @since 0.9.16
*/
public Engine(Version recommendedDefaults, Version freemarkerIncompatibleImprovements, BeansWrapper objectWrapper) {
if (recommendedDefaults == null) {
recommendedDefaults = DEFAULT_RECOMMENDED_DEFAULTS;
}
validateRecommendedDefaults(recommendedDefaults);
this.recommendedDefaults = recommendedDefaults;
if (freemarkerIncompatibleImprovements == null) {
freemarkerIncompatibleImprovements = getDefaultFreemarkerIncompatibleImprovements(recommendedDefaults);
}
fmCfg = new Configuration(freemarkerIncompatibleImprovements);
if (objectWrapper == null) {
objectWrapper = createDefaultObjectWrapper(recommendedDefaults, freemarkerIncompatibleImprovements);
}
fmCfg.setObjectWrapper(objectWrapper);
fmCfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
fmCfg.setTemplateUpdateDelayMilliseconds(Integer.MAX_VALUE - 10000);
fmCfg.setDefaultEncoding("ISO-8859-1");
fmCfg.setLocale(Locale.US);
fmCfg.setNumberFormat("0.############");
fmCfg.setLocalizedLookup(false);
fmCfg.setAPIBuiltinEnabled(true); // Because there's pp.loadData('eval', ...) anyway.
if (recommendedDefaultsGE0916(recommendedDefaults)) {
mapCommonExtensionsToOutputFormats = true;
removeFreemarkerExtensions = true;
}
templateEnv = new TemplateEnvironment(this);
}
/**
* Check if the {@code recommendedDefaults} is in the supported range.
*
* @param recommendedDefaults
* The version to validate. If {@code null}, the method returns without doing anything.
*
* @throws IllegalArgumentException
* If the specified version is out of the valid range
*
* @since 0.9.16
*/
public static void validateRecommendedDefaults(Version recommendedDefaults) {
if (recommendedDefaults == null) {
return;
}
if (recommendedDefaults.intValue() < VERSION_0_9_15.intValue()) {
throw new IllegalArgumentException("\"recommendedDefaults\" setting value \"" + recommendedDefaults
+ "\" is lower than the allowed minimum, \"" + VERSION_0_9_15 + "\".");
}
if (recommendedDefaults.intValue() > getVersion().intValue()) {
throw new IllegalArgumentException("\"recommendedDefaults\" setting value \"" + recommendedDefaults
+ "\" is higher than the current FMPP version, \"" + getVersion() + "\".");
}
}
/**
* The default value of the {@code objectWrapper} setting, when {@code null} is passed for it
* to the {@link Engine} constructor.
*/
private static BeansWrapper createDefaultObjectWrapper(Version recommendedDefaults, Version fmIncompImprovements) {
BeansWrapper objectWrapper;
if (recommendedDefaultsGE0916(recommendedDefaults)
&& fmIncompImprovements.intValue() >= Configuration.VERSION_2_3_21.intValue()) {
DefaultObjectWrapperBuilder dowb = new DefaultObjectWrapperBuilder(fmIncompImprovements);
dowb.setForceLegacyNonListCollections(false);
dowb.setIterableSupport(true);
objectWrapper = dowb.build();
} else {
if (fmIncompImprovements == null
|| fmIncompImprovements.intValue() < Configuration.VERSION_2_3_21.intValue()) {
// The old (deprecated) way:
BeansWrapper bw = fmIncompImprovements != null
? new BeansWrapper(fmIncompImprovements) : new BeansWrapper();
bw.setSimpleMapWrapper(true);
objectWrapper = bw;
} else {
BeansWrapperBuilder bwb = new BeansWrapperBuilder(fmIncompImprovements);
bwb.setSimpleMapWrapper(true);
objectWrapper = bwb.build();
}
}
return objectWrapper;
}
/**
* The default value of the {@code freemarkerIncompatibleImprovements} setting, when {@code null} is passed for it
* to the {@link Engine} constructor. This was exposed as sometimes you need this information earlier than calling
* the {@link Engine} constructor.
*
* @since 0.9.16
*/
public static Version getDefaultFreemarkerIncompatibleImprovements(Version fmppRecommendedDefaults) {
return recommendedDefaultsGE0916(fmppRecommendedDefaults)
? Configuration.VERSION_2_3_28 : Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS;
}
// -------------------------------------------------------------------------
// Processing
/**
* Processes a list of files.
*
*
The source root and output root directory must be set (non-null) prior
* to calling this method.
*
* @see #process(File, File)
*
* @param sources The list of files to process. All file must be inside
* the source root. The files will be processed in the order as they
* appear in the list, except that if you use multiple turns, they
* are re-sorted based on the associated turns (the original order
* of files is kept inside turns).
*
* @throws ProcessingException if {@code Engine.process} has
* thrown any exception. The message of this exception holds nothing
* interesting (just a static text). Call its {@code getCause()}
* method to get the exception that caused the termination. Note that
* all (so even non-checked exceptions) thrown be the engine are
* catched and wrapped by this exeption.
*/
public void process(File[] sources)
throws ProcessingException {
progListeners.notifyProgressEvent(
this,
ProgressListener.EVENT_BEGIN_PROCESSING_SESSION,
null, PMODE_NONE,
null, null);
try {
try {
setupSession();
} catch (IllegalConfigurationException e) {
throw new ProcessingException(this, null, e);
}
try {
File src;
File[] srcs = new File[sources.length];
for (int i = 0; i < sources.length; i++) {
src = sources[i].getCanonicalFile();
if (!FileUtil.isInsideOrEquals(src, srcRoot)) {
throw new IOException(
"The source file ("
+ src.getPath()
+ ") is not inside the source root ("
+ srcRoot.getPath() + ")");
}
srcs[i] = src;
}
for (; currentTurn <= maxTurn; currentTurn++) {
for (int i = 0; i < srcs.length; i++) {
if (srcs[i] != null) {
boolean done;
File out = new File(
outRoot,
FileUtil.getRelativePath(srcRoot, srcs[i]));
if (srcs[i].isDirectory()) {
done = processDir(srcs[i], out);
} else {
done = processFile(srcs[i], out, true);
}
if (done) {
srcs[i] = null;
}
}
}
}
} finally {
cleanupSession();
}
} catch (ProcessingException e) {
progListeners.notifyProgressEvent(
this,
ProgressListener.EVENT_END_PROCESSING_SESSION,
null, PMODE_NONE,
e, null);
throw e;
} catch (IOException e) {
progListeners.notifyProgressEvent(
this,
ProgressListener.EVENT_END_PROCESSING_SESSION,
null, PMODE_NONE,
e, null);
throw new ProcessingException(this, null, e);
}
progListeners.notifyProgressEvent(
this,
ProgressListener.EVENT_END_PROCESSING_SESSION,
null, PMODE_NONE,
null, null);
}
private boolean isDirMarkedWithIgnoreFile(File dir)
throws IOException {
boolean ign;
Boolean ignore = ignoredDirCache.get(dir);
if (ignore != null) {
return ignore.booleanValue();
}
if (!dir.equals(srcRoot)) {
File parentDir = dir.getParentFile();
if (parentDir != null && isDirMarkedWithIgnoreFile(parentDir)) {
ignoredDirCache.put(dir, Boolean.TRUE);
return true;
}
}
ign = new File(dir, IGNOREDIR_FILE).exists();
ignoredDirCache.put(dir, ign ? Boolean.TRUE : Boolean.FALSE);
return ign;
}
/**
* Hack to processes a single file.
*
*
If the source root and/or output root directory is not set, they
* will be set for the time of this method call to the parent directories of
* the source and output files respectively.
*
* @see #process(File[])
*
* @param src the source file (not directory). Can't be null.
* @param out the output file (not directory). Can't be null.
*
* @throws ProcessingException if {@code Engine.process} has
* thrown any exception. The message of this exception holds nothing
* interesting (just a static text). Call its {@code getCause()}
* method to get the exception that caused the termination. Note that
* all (so even non-checked exceptions) thrown be the engine are
* catched and wrapped by this exception.
*/
public void process(File src, File out)
throws ProcessingException {
progListeners.notifyProgressEvent(
this,
ProgressListener.EVENT_BEGIN_PROCESSING_SESSION,
null, PMODE_NONE,
null, null);
File oldSrcRoot = srcRoot;
File oldOutRoot = outRoot;
try {
try {
if (src == null) {
throw new IllegalArgumentException(
"The source argument can't be null.");
}
if (out == null) {
throw new IllegalArgumentException(
"The output argument can't be null.");
}
src = src.getCanonicalFile();
if (!src.exists()) {
throw new IOException(
"Source file not found: "
+ src.getPath());
}
if (src.isDirectory()) {
throw new IOException(
"Source file can't be a directory: "
+ src.getPath());
}
out = out.getCanonicalFile();
if (out.exists() && out.isDirectory()) {
throw new IOException(
"The output file can't be a directory.");
}
if (srcRoot == null) {
setSourceRoot(src.getParentFile());
}
if (outRoot == null) {
setOutputRoot(out.getParentFile());
}
try {
setupSession();
} catch (IllegalConfigurationException e) {
throw new ProcessingException(this, null, e);
}
try {
if (!FileUtil.isInsideOrEquals(src, srcRoot)) {
throw new IOException(
"The source file ("
+ src.getPath()
+ ") is not inside the source root ("
+ srcRoot.getPath() + ")");
}
if (!FileUtil.isInsideOrEquals(out, outRoot)) {
throw new IOException(
"The output file ("
+ out.getPath()
+ ") is not inside the output root ("
+ outRoot.getPath() + ")");
}
for (; currentTurn <= maxTurn; currentTurn++) {
processFile(src, out, false);
}
} finally {
cleanupSession();
}
} catch (ProcessingException e) {
progListeners.notifyProgressEvent(
this,
ProgressListener.EVENT_END_PROCESSING_SESSION,
null, PMODE_NONE,
e, null);
throw e;
} catch (IOException e) {
progListeners.notifyProgressEvent(
this,
ProgressListener.EVENT_END_PROCESSING_SESSION,
null, PMODE_NONE,
e, null);
throw new ProcessingException(this, null, e);
}
progListeners.notifyProgressEvent(
this,
ProgressListener.EVENT_END_PROCESSING_SESSION,
null, PMODE_NONE,
null, null);
} finally {
// clear auto-deduced root dirs.
if (oldSrcRoot == null) {
srcRoot = null;
}
if (oldOutRoot == null) {
outRoot = null;
}
}
}
private void setupSession()
throws IOException, IllegalConfigurationException {
if (srcRoot == null) {
throw new IllegalConfigurationException(
"The source root directory was not set.");
}
if (outRoot == null) {
throw new IllegalConfigurationException(
"The output root directory was not set.");
}
if (!srcRoot.exists()) {
throw new IOException("Source root directory does not exists.");
}
if (!srcRoot.isDirectory()) {
throw new IOException("Source root is not a directory.");
}
if (outRoot.exists() && !outRoot.isDirectory()) {
throw new IOException("Output root is not a directory.");
}
boolean done = false;
try {
if (!xpathEngine.equals(XPATH_ENGINE_DONT_SET)) {
EngineXmlUtils.setFreeMarkerXPathEngine(xpathEngine);
}
maxTurn = 1;
for (TurnChooser turnChooser : turnChoosers) {
int turn = turnChooser.turn;
if (turn > maxTurn) {
maxTurn = turn;
}
}
currentTurn = 1;
fmCfg.setTemplateLoader(new FmppTemplateLoader(this));
fmCfg.clearTemplateCache();
fmCfg.clearSharedVariables();
for (Map.Entry ent : data.entrySet()) {
try {
fmCfg.setSharedVariable(ent.getKey(), ent.getValue());
} catch (TemplateModelException e) {
throw new IllegalConfigurationException(
"Failed to convert data " + StringUtil.jQuote(ent.getKey()) + " to FreeMarker variable.",
e);
}
}
// Note: We recreate the TemplateConfigurationFactory, as things like registered OutputFormats could have
// changed.
fmCfg.setTemplateConfigurations(new FMPPTemplateConfigurationFactory());
processedFiles.clear();
ignoredDirCache.clear();
templateEnv.setupForSession();
lockParameters();
done = true;
} finally {
if (!done) {
cleanupSession();
}
}
}
private void cleanupSession() {
unlockParameters();
templateEnv.cleanAfterSession();
processedFiles.clear();
ignoredDirCache.clear();
fmCfg.clearTemplateCache();
fmCfg.clearSharedVariables();
}
private boolean processDir(File srcDir, File dstDir)
throws IOException, ProcessingException {
if (isDirMarkedWithIgnoreFile(srcDir)) {
return true;
}
String name = srcDir.getName();
if (ignoreCvsFiles) {
if (name.equals("CVS")
|| (!csPathCmp && name.equalsIgnoreCase("CVS"))) {
return true;
}
}
if (ignoreSvnFiles) {
if (name.equals(".svn")
|| (!csPathCmp && name.equalsIgnoreCase(".svn"))) {
return true;
}
}
if (alwaysCrateDirs || new File(srcDir, CREATEDIR_FILE).isFile()) {
if (!dstDir.exists()) {
if (!dstDir.mkdirs()) {
throw new IOException(
"Failed to create directory: "
+ dstDir.getAbsolutePath());
}
progListeners.notifyProgressEvent(
this,
ProgressListener.EVENT_CREATED_EMPTY_DIR,
srcDir,
PMODE_NONE, null, null);
}
}
if (!dontTraverseDirs) {
File[] dir = srcDir.listFiles();
for (int i = 0; i < dir.length; i++) {
File sf = dir[i];
String fn = sf.getName();
File df = new File(dstDir, fn);
if (sf.isDirectory()) {
processDir(sf, df);
} else {
processFile(sf, df, true);
}
}
}
return false;
}
private boolean processFile(File sf, File df, boolean allowOutFAdj)
throws IOException, ProcessingException {
if (isDirMarkedWithIgnoreFile(
sf.getParentFile().getCanonicalFile())) {
return true;
}
if (sf.getName().equalsIgnoreCase(CREATEDIR_FILE)) {
File srcDir = sf.getParentFile();
// Re-check with the comparison rules of the file-system
if (new File(srcDir, CREATEDIR_FILE).exists()) {
File dstDir = df.getParentFile();
if (!dstDir.exists()) {
if (!dstDir.mkdirs()) {
throw new IOException(
"Failed to create directory: "
+ dstDir.getAbsolutePath());
}
progListeners.notifyProgressEvent(
this,
ProgressListener.EVENT_CREATED_EMPTY_DIR,
srcDir,
PMODE_NONE, null, null);
}
return true;
}
}
if (currentTurn != getTurn(sf)) {
return false;
}
if (!processedFiles.add(sf)) {
return true;
}
int pmode = getProcessingMode(sf);
Throwable catchedExc = null;
try {
if (allowOutFAdj && pmode != Engine.PMODE_IGNORE) {
df = adjustOutputFileName(df);
}
if (!expertMode && pmode != Engine.PMODE_IGNORE) {
if (sf.equals(df)) {
throw new IOException(
"The input and output files are the same ("
+ sf.getPath()
+ "); if you want to allow this, "
+ "you should turn on expert mode.");
}
}
if (pmode != Engine.PMODE_IGNORE
&& (skipUnchanged == SKIP_ALL
|| (skipUnchanged == SKIP_STATIC
&& pmode == Engine.PMODE_COPY))) {
long dfl = df.lastModified();
long sfl = sf.lastModified();
if (df.exists() && dfl > 0 && sfl > 0 && dfl == sfl) {
progListeners.notifyProgressEvent(
this,
ProgressListener.EVENT_SOURCE_NOT_MODIFIED,
sf, pmode,
null, null);
return true; //!
}
}
} catch (Throwable e) {
// delay the throwing of exc. as if it was happen while processing
catchedExc = e;
}
progListeners.notifyProgressEvent(
this,
ProgressListener.EVENT_BEGIN_FILE_PROCESSING,
sf, pmode,
null, null);
try {
if (catchedExc != null) {
throw catchedExc;
}
switch (pmode) {
case PMODE_EXECUTE:
executeFile(sf, df);
break;
case PMODE_COPY:
File dstDir;
dstDir = df.getParentFile();
if (dstDir != null) {
dstDir.mkdirs();
}
FileUtil.copyFile(sf, df);
break;
case PMODE_RENDER_XML:
renderXmlFile(sf, df);
break;
case PMODE_IGNORE:
break;
default:
throw new BugException(
"Bad processing mode in the procModeChoosers:" + pmode);
}
} catch (Throwable e) {
catchedExc = e;
progListeners.notifyProgressEvent(
this,
ProgressListener.EVENT_END_FILE_PROCESSING,
sf, pmode,
e, null);
}
if (catchedExc == null) {
progListeners.notifyProgressEvent(
this,
ProgressListener.EVENT_END_FILE_PROCESSING,
sf, pmode,
null, null);
} else {
// OutOfMemoryError-s can cause Java applications to enter an
// inconsistent state, so it's better stop the session.
if (stopOnError || catchedExc instanceof OutOfMemoryError) {
throw new ProcessingException(this, sf, catchedExc);
}
}
return true;
}
private void executeFile(File sf, File df)
throws ProcessingException, DataModelBuildingException,
TemplateException, IOException {
Template template
= fmCfg.getTemplate(FileUtil.pathToUnixStyle(
FileUtil.getRelativePath(srcRoot, sf)));
String outEnc = getOutputEncoding();
if (outputEncoding.equalsIgnoreCase(PARAMETER_VALUE_SOURCE)) {
outEnc = template.getEncoding();
}
FmppOutputWriter out = new FmppFileOutputWriter(this, df, outEnc);
boolean done = false;
try {
templateEnv.execute(template, out, sf, null, null, null);
done = true;
} finally {
out.close(!done);
}
}
private void renderXmlFile(File sf, File df)
throws ProcessingException, DataModelBuildingException,
TemplateException, IOException, InstallationException,
GenericProcessingException {
final XmlRenderingConfiguration xrc;
Object loadedDoc = null; // this is an org.w3c.Document
boolean isLoadedDocumentValidated = false;
{
String sfPathForComparison = null;
int xrccln = xmlRendCfgCntrs.size();
int xrccIdx;
XmlRenderingConfiguration curXRC = null;
findMatchingXRC: for (xrccIdx = 0; xrccIdx < xrccln; xrccIdx++) {
XmlRenderingCfgContainer curXRCC = xmlRendCfgCntrs.get(xrccIdx);
curXRC = curXRCC.xmlRenderingCfg;
// Filter: ifSourceIs
{
int ln = curXRCC.compiledPathPatterns.length;
if (ln != 0) {
if (sfPathForComparison == null) {
sfPathForComparison = normalizePathForComparison(
FileUtil.pathToUnixStyle(
FileUtil.getRelativePath(srcRoot, sf)));
}
int i;
for (i = 0; i < ln; i++) {
if (curXRCC.compiledPathPatterns[i].matcher(sfPathForComparison).matches()) {
break;
}
}
if (i == ln) {
// curXRC was excluded
continue findMatchingXRC;
}
}
} // end Filter: ifSourceIs
// At this point: we know that "ifSourceIs" doesn't exclude curXRC
// Filter: ifDocumentElementIs
{
int ln = curXRC.getDocumentElementLocalNames().size();
if (ln != 0) {
if (loadedDoc == null) {
Object o = curXRC.getXmlDataLoaderOptions().get("validate");
if (o == null) {
o = getValidateXml() ? Boolean.TRUE : Boolean.FALSE;
}
isLoadedDocumentValidated = Boolean.TRUE.equals(o);
loadXml: while (true) {
try {
loadedDoc = EngineXmlUtils.loadXmlFile(
this, sf, isLoadedDocumentValidated);
} catch (Exception e) {
if (isLoadedDocumentValidated) {
isLoadedDocumentValidated = false;
// Retry without validation:
continue loadXml;
}
throw new DataModelBuildingException(
"Failed to load XML source file.", e);
}
break loadXml;
}
}
// At this point: loadedDoc is non-null
List localNames = curXRC.getDocumentElementLocalNames();
List namespaces = curXRC.getDocumentElementNamespaces();
int i;
for (i = 0; i < ln; i++) {
if (EngineXmlUtils.documentElementEquals(
loadedDoc,
(String) namespaces.get(i),
(String) localNames.get(i))) {
break;
}
}
if (i == ln) {
// curXRC was excluded
continue findMatchingXRC;
}
}
} // end Filter: ifDocumentElementIs
// At this point: we know that "ifDocumentElementIs" doesn't exclude curXRC
// Nothing has excluded it, so curXRC is matching:
break findMatchingXRC;
} // findRendering
xrc = xrccIdx != xrccln ? curXRC : null;
} // end find matching XRC
if (xrc == null) {
throw new GenericProcessingException(
"The source file has to be processed in "
+ "\"renderXml\" mode, but there is no matching "
+ "XML rendering configuration for it. "
+ "(Check the if... options of the XML rendering "
+ "configurations)");
}
if (xrc.getCopy()) {
File dstDir;
dstDir = df.getParentFile();
if (dstDir != null) {
dstDir.mkdirs();
}
FileUtil.copyFile(sf, df);
} else {
Object xmlDLValidateOpt = xrc.getXmlDataLoaderOptions().get("validate");
if (xmlDLValidateOpt == null) {
xmlDLValidateOpt = Boolean.valueOf(getValidateXml());
}
boolean doctMustBeValidated = Boolean.TRUE.equals(xmlDLValidateOpt);
if (isLoadedDocumentValidated != doctMustBeValidated) {
loadedDoc = null;
}
if (loadedDoc == null) {
try {
loadedDoc = EngineXmlUtils.loadXmlFile(this, sf, doctMustBeValidated);
} catch (Exception e) {
throw new DataModelBuildingException(
"Failed to load the XML source file.", e);
}
}
TemplateNodeModel wrappedDoc;
List