Java tutorial
/* * Copyright 2014 Igor Maznitsa (http://www.igormaznitsa.com). * * 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 com.igormaznitsa.jcp.context; import com.igormaznitsa.jcp.containers.FileInfoContainer; import com.igormaznitsa.jcp.containers.TextFileDataContainer; import com.igormaznitsa.jcp.exceptions.FilePositionInfo; import com.igormaznitsa.jcp.exceptions.PreprocessorException; import com.igormaznitsa.jcp.expression.Value; import com.igormaznitsa.jcp.extension.PreprocessorExtension; import com.igormaznitsa.jcp.logger.PreprocessorLogger; import com.igormaznitsa.jcp.logger.SystemOutLogger; import com.igormaznitsa.jcp.utils.PreprocessorUtils; import java.io.*; import java.nio.charset.Charset; import java.util.*; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.apache.commons.io.FilenameUtils; import com.igormaznitsa.meta.annotation.MustNotContainNull; import static com.igormaznitsa.meta.common.utils.Assertions.assertNotNull; import com.igormaznitsa.meta.common.utils.Assertions; /** * The preprocessor context class is a main class which contains all options of the preprocessor and allows to work with variables in expressions * * @author Igor Maznitsa (igor.maznitsa@igormaznitsa.com) */ public final class PreprocessorContext { public static final String DEFAULT_SOURCE_DIRECTORY = "." + File.separatorChar; public static final String DEFAULT_DEST_DIRECTORY = ".." + File.separatorChar + "preprocessed"; public static final String DEFAULT_PROCESSING_EXTENSIONS = "java,txt,htm,html"; public static final String DEFAULT_EXCLUDED_EXTENSIONS = "xml"; public static final String DEFAULT_CHARSET = "UTF8"; private boolean verbose = false; private boolean removeComments = false; private boolean clearDestinationDirectoryBefore = false; private boolean fileOutputDisabled = false; private boolean keepNonExecutingLines = false; private boolean careForLastNextLine = false; private boolean compareDestination = false; private boolean allowWhitespace = false; private boolean preserveIndent = false; private boolean copyFileAttributes = false; private boolean unknownVariableAsFalse = false; private String sourceDirectories; private String destinationDirectory; private File destinationDirectoryFile; private File[] sourceDirectoryFiles; private Set<String> processingFileExtensions = new HashSet<String>( Arrays.asList(PreprocessorUtils.splitForChar(DEFAULT_PROCESSING_EXTENSIONS, ','))); private Set<String> excludedFileExtensions = new HashSet<String>( Arrays.asList(PreprocessorUtils.splitForChar(DEFAULT_EXCLUDED_EXTENSIONS, ','))); private PreprocessorExtension preprocessorExtension; private String inCharacterEncoding = DEFAULT_CHARSET; private String outCharacterEncoding = DEFAULT_CHARSET; private final Map<String, Value> globalVarTable = new HashMap<String, Value>(); private final Map<String, Value> localVarTable = new HashMap<String, Value>(); private final Map<String, SpecialVariableProcessor> mapVariableNameToSpecialVarProcessor = new HashMap<String, SpecialVariableProcessor>(); private final Map<String, Object> sharedResources = new HashMap<String, Object>(); private PreprocessorLogger preprocessorLogger = new SystemOutLogger(); private final List<File> configFiles = new ArrayList<File>(); private transient PreprocessingState currentState; private final boolean cloned; private final TextFileDataContainer currentInCloneSource; private String[] excludedFolderPatterns = new String[0]; /** * The constructor */ public PreprocessorContext() { this.currentState = new PreprocessingState(this, this.inCharacterEncoding, this.outCharacterEncoding); setSourceDirectories(DEFAULT_SOURCE_DIRECTORY).setDestinationDirectory(DEFAULT_DEST_DIRECTORY); registerSpecialVariableProcessor(new JCPSpecialVariableProcessor()); registerSpecialVariableProcessor(new EnvironmentVariableProcessor()); this.cloned = false; this.currentInCloneSource = null; } /** * Set patterns for excluded folders. * @param patterns array contains Ant path patterns */ public void setExcludedFolderPatterns(@MustNotContainNull @Nonnull final String... patterns) { final String[] value = Assertions.assertDoesntContainNull(Assertions.assertNotNull(patterns)); final String[] normalized = new String[value.length]; for (int i = 0; i < value.length; i++) { normalized[i] = FilenameUtils.normalize(value[i], true); } this.excludedFolderPatterns = normalized; } /** * Get patterns for excluded folders. * @return array of patterns in Ant pattern format */ @Nonnull @MustNotContainNull public String[] getExcludedFolderPatterns() { return this.excludedFolderPatterns.clone(); } /** * Set the flag to care to be precise in processing the last file next line char * * @param flag true to turn on the mode, false to turn off */ public void setCareForLastNextLine(final boolean flag) { this.careForLastNextLine = flag; } public boolean isCareForLastNextLine() { return this.careForLastNextLine; } /** * Check that the preprocessor context is a clone of another context. * * @return true if the context is a clone, false otherwise */ public boolean isCloned() { return this.cloned; } /** * Make clone of a preprocessor context but without cloning state. * * @param context the context to be cloned, must not be null. */ public PreprocessorContext(@Nonnull final PreprocessorContext context) { assertNotNull("Source context must not be null", context); this.verbose = context.verbose; this.removeComments = context.removeComments; this.clearDestinationDirectoryBefore = context.clearDestinationDirectoryBefore; this.fileOutputDisabled = context.fileOutputDisabled; this.keepNonExecutingLines = context.keepNonExecutingLines; this.allowWhitespace = context.allowWhitespace; this.preserveIndent = context.preserveIndent; this.sourceDirectories = context.sourceDirectories; this.destinationDirectory = context.destinationDirectory; this.destinationDirectoryFile = context.destinationDirectoryFile; this.sourceDirectoryFiles = context.sourceDirectoryFiles.clone(); this.copyFileAttributes = context.copyFileAttributes; this.careForLastNextLine = context.careForLastNextLine; this.processingFileExtensions.clear(); this.processingFileExtensions.addAll(context.processingFileExtensions); this.excludedFileExtensions.clear(); this.excludedFileExtensions.addAll(context.excludedFileExtensions); this.unknownVariableAsFalse = context.unknownVariableAsFalse; this.preprocessorExtension = context.preprocessorExtension; this.inCharacterEncoding = context.inCharacterEncoding; this.outCharacterEncoding = context.outCharacterEncoding; this.compareDestination = context.compareDestination; this.globalVarTable.putAll(context.globalVarTable); this.localVarTable.putAll(context.localVarTable); this.excludedFolderPatterns = context.excludedFolderPatterns.clone(); this.mapVariableNameToSpecialVarProcessor.putAll(context.mapVariableNameToSpecialVarProcessor); this.sharedResources.putAll(context.sharedResources); this.configFiles.addAll(context.configFiles); this.currentState = assertNotNull(context.currentState); this.cloned = true; this.preprocessorLogger = context.preprocessorLogger; final PreprocessingState theState = context.getPreprocessingState(); this.currentInCloneSource = theState.peekFile(); } /** * Set the logger to print information and error messages * * @param logger a logger to be used for output, it can be null */ public void setPreprocessorLogger(@Nullable final PreprocessorLogger logger) { preprocessorLogger = logger; } /** * It allows to register a special variable processor which can process some special global variables * * @param processor a variable processor to be registered, it must not be null * @see SpecialVariableProcessor */ public void registerSpecialVariableProcessor(@Nonnull final SpecialVariableProcessor processor) { assertNotNull("Processor is null", processor); for (final String varName : processor.getVariableNames()) { assertNotNull("A Special Var name is null", varName); if (mapVariableNameToSpecialVarProcessor.containsKey(varName)) { throw new IllegalStateException("There is already defined processor for " + varName); } mapVariableNameToSpecialVarProcessor.put(varName, processor); } } /** * Print an information into the current log * * @param text a String to be printed into the information log, it can be null */ public void logInfo(@Nullable final String text) { if (text != null && this.preprocessorLogger != null) { this.preprocessorLogger.info(text); } } /** * Print an information about an error into the current log * * @param text a String to be printed into the error log, it can be null */ public void logError(@Nullable final String text) { if (text != null && this.preprocessorLogger != null) { this.preprocessorLogger.error(text); } } /** * Print some debug info into the current log * * @param text a String to be printed into the error log, it can be null * @since 6.0.1 */ public void logDebug(@Nullable final String text) { if (text != null && this.preprocessorLogger != null) { this.preprocessorLogger.debug(text); } } /** * Print an information about a warning situation into the current log * * @param text a String to be printed into the warning log, it can be null */ public void logWarning(@Nullable final String text) { if (text != null || this.preprocessorLogger != null) { this.preprocessorLogger.warning(text); } } /** * Set the remove comments flag * * @param removingComments the flag to set, true if comments must be removed from the result files, otherwise else * @return the preprocessor context instance */ @Nonnull public PreprocessorContext setRemoveComments(final boolean removingComments) { this.removeComments = removingComments; return this; } /** * It returns the flag shows that all comments must be removed from the result * * @return true if comments must be returned, otherwise false */ public boolean isRemoveComments() { return this.removeComments; } /** * It allows to disable all writing operations of the preprocessor * * @param flag true if preprocessor must not make any writing operations, otherwise false */ public void setFileOutputDisabled(final boolean flag) { fileOutputDisabled = flag; } /** * Check that writing operations is disabled * * @return true if writing operations disabled, otherwise false */ public boolean isFileOutputDisabled() { return fileOutputDisabled; } /** * Set flag to allow whitespace between directive and comment chars. * @param flag true if whitespace allowed, false otherwise */ public void setAllowWhitespace(final boolean flag) { this.allowWhitespace = flag; } /** * Get flag that whitespace allowed between directive and comment. * @return true if whitespace allowed, false otherwise */ public boolean isAllowWhitespace() { return this.allowWhitespace; } /** * Set flag to interpret unknown variable value as FALSE. * @param flag true to turn on mode when unknown variable will be recognized as FALSE */ public void setUnknownVariableAsFalse(final boolean flag) { this.unknownVariableAsFalse = flag; } /** * Get flag shows that unknown variable is recognized as FALSE. * @return true if unknown variable must be recognized as FALSE. */ public boolean isUnknownVariableAsFalse() { return this.unknownVariableAsFalse; } /** * Set flag to control whether prefixes "//$", "//$$" should replaced * with equal length whitespace strings rather than just removed. * @param flag true enables preserve-indent, false disables it */ public void setPreserveIndent(final boolean flag) { this.preserveIndent = flag; } /** * Get flag indicating whether preserve-indent is enabled or disabled. * @return true if preserve-indent is enabled, false otherwise */ public boolean isPreserveIndent() { return this.preserveIndent; } /** * Set source directories * * @param directories semi separated list of source directories, must not be null * @return this preprocessor context instance */ @Nonnull public PreprocessorContext setSourceDirectories(@Nonnull final String directories) { assertNotNull("Source directory is null", directories); this.sourceDirectories = directories; this.sourceDirectoryFiles = getParsedSourceDirectoryAsFiles(); return this; } /** * Set a shared source, it is an object saved into the inside map for a name * * @param name the name for the saved project, must not be null * @param obj the object to be saved in, must not be null */ public void setSharedResource(@Nonnull final String name, @Nonnull final Object obj) { assertNotNull("Name is null", name); assertNotNull("Object is null", obj); sharedResources.put(name, obj); } /** * Get a shared source from inside map * * @param name the name of the needed object, it must not be null * @return a cached object or null if it is not found */ @Nullable public Object getSharedResource(@Nonnull final String name) { assertNotNull("Name is null", name); return sharedResources.get(name); } /** * Remove a shared object from the inside map for its name * * @param name the object name, it must not be null * @return removing object or null if it is not found */ @Nullable public Object removeSharedResource(@Nonnull final String name) { assertNotNull("Name is null", name); return sharedResources.remove(name); } /** * Get the source directories as semi separated string * * @return the current source directories semi separated list */ @Nonnull public String getSourceDirectories() { return sourceDirectories; } /** * Get the current source directories as a file array * * @return the current source directories as a file array */ @Nonnull @MustNotContainNull public File[] getSourceDirectoryAsFiles() { return sourceDirectoryFiles; } /** * Inside auxiliary method to parse the source directories list into file array * * @return parsed file list, each file must exist and be a directory */ @Nonnull @MustNotContainNull private File[] getParsedSourceDirectoryAsFiles() { final String[] splitted = PreprocessorUtils.splitForChar(sourceDirectories, ';'); final File[] result = new File[splitted.length]; int index = 0; for (final String dirName : splitted) { final File dir = new File(dirName); if (!dir.isDirectory()) { throw new IllegalStateException( "Can't find a source directory [" + PreprocessorUtils.getFilePath(dir) + ']'); } result[index++] = dir; } return result; } /** * Set a destination directory for result files * * @param directory a path to the directory as String, it must not be null * @return this preprocessor context instance */ @Nonnull public PreprocessorContext setDestinationDirectory(@Nonnull final String directory) { assertNotNull("Directory is null", directory); this.destinationDirectory = directory; destinationDirectoryFile = new File(this.destinationDirectory); return this; } /** * Get the current destination directory as a File object * * @return the current destination directory as an object */ @Nonnull public File getDestinationDirectoryAsFile() { return destinationDirectoryFile; } /** * Get the string representation of the destination directory * * @return the current destination directory as a String */ @Nonnull public String getDestinationDirectory() { return destinationDirectory; } /** * Set file extensions of files to be preprocessed, it is a comma separated list * * @param extensions comma separated extensions list of file extensions to be preprocessed, must not be null * @return this preprocessor context */ @Nonnull public PreprocessorContext setProcessingFileExtensions(@Nonnull final String extensions) { assertNotNull("Argument is null", extensions); processingFileExtensions = new HashSet<String>( Arrays.asList(PreprocessorUtils.splitExtensionCommaList(extensions))); return this; } /** * Get file extensions of files to be preprocessed as a string array * * @return a string array of file extensions to be preprocessed */ @Nonnull @MustNotContainNull public String[] getProcessingFileExtensions() { return processingFileExtensions.toArray(new String[processingFileExtensions.size()]); } /** * Check that a file is allowed to be preprocessed fo its extension * * @param file a file to be checked * @return true if the file is allowed, false otherwise */ public final boolean isFileAllowedToBeProcessed(@Nullable final File file) { if (file == null || !file.exists() || file.isDirectory() || file.length() == 0) { return false; } return processingFileExtensions.contains(PreprocessorUtils.getFileExtension(file)); } /** * Check that a file is excluded from preprocessing and coping actions * * @param file a file to be checked * @return true if th file must be excluded, otherwise false */ public final boolean isFileExcludedFromProcess(@Nullable final File file) { final boolean result; if (file != null && file.isFile()) { result = excludedFileExtensions.contains(PreprocessorUtils.getFileExtension(file)); } else { result = false; } return result; } /** * Set comma separated list of file extensions to be excluded from preprocessing * * @param extensions a comma separated file extension list, it must not be null * @return this preprocessor context */ @Nonnull public PreprocessorContext setExcludedFileExtensions(@Nonnull final String extensions) { assertNotNull("Argument is null", extensions); excludedFileExtensions = new HashSet<String>( Arrays.asList(PreprocessorUtils.splitExtensionCommaList(extensions))); return this; } /** * Get excluded file extension list as a string array * * @return a string array contains file extensions to be excluded from preprocessing act */ @Nonnull @MustNotContainNull public String[] getExcludedFileExtensions() { return excludedFileExtensions.toArray(new String[excludedFileExtensions.size()]); } /** * Set the flag to clear the destination directory before preprocessing * * @param flag true if the directory must be cleaned, otherwise false * @return this preprocessor context */ @Nonnull public PreprocessorContext setClearDestinationDirBefore(final boolean flag) { this.clearDestinationDirectoryBefore = flag; return this; } /** * Get the flag to clear the destination directory before preprocessing * * @return true if the directory must be cleaned, otherwise false */ public boolean doesClearDestinationDirBefore() { return this.clearDestinationDirectoryBefore; } /** * Set a local variable value * * @param name the variable name, must not be null, remember that the name will be normalized and will be entirely in lower case * @param value the value for the variable, it must not be null * @return this preprocessor context * @see Value */ @Nonnull public PreprocessorContext setLocalVariable(@Nonnull final String name, @Nonnull final Value value) { assertNotNull("Variable name is null", name); assertNotNull("Value is null", value); final String normalized = assertNotNull(PreprocessorUtils.normalizeVariableName(name)); if (normalized.isEmpty()) { throw makeException("Not defined variable name", null); } if (mapVariableNameToSpecialVarProcessor.containsKey(normalized) || globalVarTable.containsKey(normalized)) { throw makeException("Attempting to set either a global variable or a special variable as a local one [" + normalized + ']', null); } localVarTable.put(normalized, value); return this; } /** * Remove a local variable value from the context. * * @param name the variable name, must not be null, remember that the name will be normalized and will be entirely in lower case * @return this preprocessor context * @see Value */ @Nonnull public PreprocessorContext removeLocalVariable(@Nonnull final String name) { assertNotNull("Variable name is null", name); final String normalized = assertNotNull(PreprocessorUtils.normalizeVariableName(name)); if (normalized.isEmpty()) { throw makeException("Empty variable name", null); } if (mapVariableNameToSpecialVarProcessor.containsKey(normalized) || globalVarTable.containsKey(normalized)) { throw makeException( "Attempting to remove either a global variable or a special variable as a local one [" + normalized + ']', null); } if (isVerbose()) { logForVerbose("Removing local variable '" + normalized + "\'"); } localVarTable.remove(normalized); return this; } /** * Remove a global variable value from the context. * * @param name the variable name, must not be null, remember that the name will be normalized and will be entirely in lower case * @return this preprocessor context * @see Value */ @Nonnull public PreprocessorContext removeGlobalVariable(@Nonnull final String name) { assertNotNull("Variable name is null", name); final String normalized = assertNotNull(PreprocessorUtils.normalizeVariableName(name)); if (normalized.isEmpty()) { throw makeException("Empty variable name", null); } if (mapVariableNameToSpecialVarProcessor.containsKey(normalized)) { throw makeException("Attempting to remove a special variable as a global one [" + normalized + ']', null); } if (isVerbose()) { logForVerbose("Removing global variable '" + normalized + "\'"); } globalVarTable.remove(normalized); return this; } /** * Get a local variable value * * @param name the name for the variable, it can be null. The name will be normalized to allowed one. * @return null either if the name is null or the variable is not found, otherwise its value */ @Nullable public Value getLocalVariable(@Nullable final String name) { if (name == null) { return null; } final String normalized = assertNotNull(PreprocessorUtils.normalizeVariableName(name)); if (normalized.isEmpty()) { return null; } return localVarTable.get(normalized); } /** * Check that a local variable for a name is presented * * @param name the checking name, it will be normalized to the support format and can be null * @return false either if the name is null or there is not any local variable for the name, otherwise true */ public boolean containsLocalVariable(@Nullable final String name) { if (name == null) { return false; } final String normalized = assertNotNull(PreprocessorUtils.normalizeVariableName(name)); if (normalized.isEmpty()) { return false; } return localVarTable.containsKey(normalized); } /** * Remove all local variables from the inside storage * * @return this preprocessor context */ @Nonnull public PreprocessorContext clearLocalVariables() { localVarTable.clear(); return this; } /** * Set a global variable value * * @param name the variable name, it must not be null and will be normalized to the supported format * @param value the variable value, it must not be null * @return this preprocessor context */ @Nonnull public PreprocessorContext setGlobalVariable(@Nonnull final String name, @Nonnull final Value value) { assertNotNull("Variable name is null", name); final String normalizedName = assertNotNull(PreprocessorUtils.normalizeVariableName(name)); if (normalizedName.isEmpty()) { throw makeException("Name is empty", null); } assertNotNull("Value is null", value); if (mapVariableNameToSpecialVarProcessor.containsKey(normalizedName)) { mapVariableNameToSpecialVarProcessor.get(normalizedName).setVariable(normalizedName, value, this); } else { if (isVerbose()) { final String valueAsStr = value.toString(); if (globalVarTable.containsKey(normalizedName)) { logForVerbose("Replacing global variable [" + normalizedName + '=' + valueAsStr + ']'); } else { logForVerbose("Defining new global variable [" + normalizedName + '=' + valueAsStr + ']'); } } globalVarTable.put(normalizedName, value); } return this; } /** * Check that there is a named global variable in the inside storage * * @param name the checking name, it will be normalized to the supported format, it can be null * @return true if such variable is presented for its name in the inside storage, otherwise false (also it is false if the name is null) */ public boolean containsGlobalVariable(@Nullable final String name) { if (name == null) { return false; } final String normalized = assertNotNull(PreprocessorUtils.normalizeVariableName(name)); if (normalized.isEmpty()) { return false; } return mapVariableNameToSpecialVarProcessor.containsKey(normalized) || globalVarTable.containsKey(normalized); } /** * Find value among local and global variables for a name. It finds in the order: special processors, local variables, global variables * * @param name the name for the needed variable, it will be normalized to the supported format * @param enforceUnknownVarAsNull if true then state of the unknownVariableAsFalse flag in context will be ignored * @return false if either the variable is not found or the name is null, otherwise the variable value */ @Nullable public Value findVariableForName(@Nullable final String name, final boolean enforceUnknownVarAsNull) { if (name == null) { return null; } final String normalized = assertNotNull(PreprocessorUtils.normalizeVariableName(name)); if (normalized.isEmpty()) { return null; } final SpecialVariableProcessor processor = mapVariableNameToSpecialVarProcessor.get(normalized); if (processor != null) { return processor.getVariable(normalized, this); } final Value val = getLocalVariable(normalized); if (val != null) { return val; } Value result = globalVarTable.get(normalized); if (result == null && !enforceUnknownVarAsNull && this.unknownVariableAsFalse) { logDebug("Unknown variable '" + name + "' is replaced by FALSE!"); result = Value.BOOLEAN_FALSE; } return result; } /** * Check that there is a global variable with such name. * * @param variableName a name to be checked, can be null * @return false if there is not such variable or it is null, true if such global or special variable exists */ public boolean isGlobalVariable(@Nullable final String variableName) { boolean result = false; if (variableName != null) { final String normalized = PreprocessorUtils.normalizeVariableName(variableName); result = this.globalVarTable.containsKey(normalized) || mapVariableNameToSpecialVarProcessor.containsKey(normalized); } return result; } /** * Check that there is a local variable with such name. * * @param variableName a name to be checked, can be null * @return false if there is not such variable or it is null, true if such local variable exists */ public boolean isLocalVariable(@Nullable final String variableName) { boolean result = false; if (variableName != null) { final String normalized = PreprocessorUtils.normalizeVariableName(variableName); result = this.localVarTable.containsKey(normalized); } return result; } /** * Set the verbose flag * * @param flag true if the preprocessor must be verbose, otherwise false * @return this preprocessor context */ @Nonnull public PreprocessorContext setVerbose(final boolean flag) { verbose = flag; return this; } /** * Check the verbose flag * * @return true if the preprocessor must be verbose, otherwise false */ public boolean isVerbose() { return verbose; } /** * Set the flag to check before saving if the content changed. * * @param flag true if to check, false otherwise * @return the preprocessor context */ @Nonnull public PreprocessorContext setCompareDestination(final boolean flag) { this.compareDestination = flag; return this; } /** * Check the flag to check content of existing file before saving. * * @return true if the content should be checked and new content must not be replaced if it is the same */ public boolean isCompareDestination() { return this.compareDestination; } /** * Set the flag to keep lines as commented ones * * @param flag true if the preprocessor must keep non-executing lines, otherwise false * @return this preprocessor context */ @Nonnull public PreprocessorContext setKeepLines(final boolean flag) { keepNonExecutingLines = flag; return this; } /** * Check that the preprocessor must keep lines as commented ones * * @return true if the preprocessor must keep lines, false otherwise */ public boolean isKeepLines() { return keepNonExecutingLines; } /** * Check that the preprocessor must copy file attributes. * * @return true if the preprocessor must copy file attributes, false otherwise. */ public boolean isCopyFileAttributes() { return this.copyFileAttributes; } /** * Set the flag to copy file attributes. * @param value true if file attributes must be copied, false otherwise. * @return the preprocessor context */ @Nonnull public PreprocessorContext setCopyFileAttributes(final boolean value) { this.copyFileAttributes = value; return this; } /** * Set a preprocessor extension, it is a module implements the PreprocessorExtension interface which can process and get some calls from a preprocessor during its work * * @param extension an object implements the PreprocessorExtension interface, it can be null * @return this preprocessor context * @see PreprocessorExtension */ @Nonnull public PreprocessorContext setPreprocessorExtension(@Nullable final PreprocessorExtension extension) { this.preprocessorExtension = extension; return this; } /** * Get the current preprocessor extension * * @return the current preprocessor extension, it can be null * @see PreprocessorExtension */ @Nullable public PreprocessorExtension getPreprocessorExtension() { return preprocessorExtension; } /** * Set the character encoding for reading texts, it must be supported by the Java platform else an exception will be thrown * * @param characterEncoding a character encoding as a String, it must not be null and must be supported by the Java platform * @return this preprocessor context */ @Nonnull public PreprocessorContext setInCharacterEncoding(@Nonnull final String characterEncoding) { assertNotNull("Value is null", characterEncoding); if (!Charset.isSupported(characterEncoding)) { throw makeException("Unsupported character encoding [" + characterEncoding + ']', null); } this.inCharacterEncoding = characterEncoding; return this; } /** * Set the output texts character encoding, it must be supported by the Java platform else an exception will be thrown * * @param characterEncoding a character encoding as a String, it must not be null and must be supported by the Java platform * @return this preprocessor context */ @Nonnull public PreprocessorContext setOutCharacterEncoding(@Nonnull final String characterEncoding) { if (!Charset.isSupported(characterEncoding)) { throw makeException("Unsupported character encoding [" + characterEncoding + ']', null); } this.outCharacterEncoding = characterEncoding; return this; } /** * Get the current character encoding for text reading * * @return the current read texts character encoding as a String */ @Nonnull public String getInCharacterEncoding() { return inCharacterEncoding; } /** * Get the current character encoding for text writing * * @return the current text writing character encoding as a String */ @Nonnull public String getOutCharacterEncoding() { return outCharacterEncoding; } /** * It allows to create a File object for its path subject to the destination directory path * * @param path the path to the file, it must not be null * @return a generated File object for the path */ @Nonnull public File createDestinationFileForPath(@Nonnull final String path) { assertNotNull("Path is null", path); if (path.isEmpty()) { throw makeException("File name is empty", null); } return new File(getDestinationDirectoryAsFile(), path); } /** * It finds a file for its path among files in source folder, it is prohibited to return files out of preprocessing folders. * * @param path the path to the needed file, it must not be null and the file must exist and be a file and be among files in preprocessing source folders * @return detected file object for the path * @throws IOException if it is impossible to find a file for the path */ @Nonnull public File findFileInSourceFolder(@Nonnull final String path) throws IOException { if (path == null) { throw makeException("Path is null", null); } if (path.isEmpty()) { throw makeException("Path is empty", null); } File result = null; final TextFileDataContainer theFile = currentState.peekFile(); final String parentDir = theFile == null ? null : theFile.getFile().getParent(); final File resultFile = new File(path); if (resultFile.isAbsolute()) { // absolute path // check that the file is a child of a preprocessing source root else usage of the file is prohibited final String normalizedPath = FilenameUtils.normalizeNoEndSeparator(resultFile.getAbsolutePath()); for (final File root : getSourceDirectoryAsFiles()) { final String rootNormalizedPath = FilenameUtils.normalizeNoEndSeparator(root.getAbsolutePath()) + File.separatorChar; if (normalizedPath.startsWith(rootNormalizedPath)) { result = resultFile; break; } } if (result == null) { throw makeException("Can't find file for path \'" + path + "\' in preprocessing source folders, allowed usage only files in preprocessing source folders!", null); } else if (!result.isFile()) { throw makeException("File \'" + result + "\' is either not found or not a file", null); } } else if (parentDir != null) { // relative path result = new File(parentDir, path); } else { final List<File> setOfFoundFiles = new ArrayList<File>(); for (final File root : getSourceDirectoryAsFiles()) { final File variant = new File(root, path); if (variant.exists() && variant.isFile()) { setOfFoundFiles.add(variant); } } if (setOfFoundFiles.size() == 1) { result = setOfFoundFiles.get(0); } else if (setOfFoundFiles.isEmpty()) { result = null; } else { throw makeException("Found several variants for path \'" + path + "\' in different source roots", null); } if (result == null) { throw makeException("Can't find file for path \'" + path + "\' among source files registered for preprocessing.", null); } else if (!result.isFile()) { throw makeException( "File \'" + PreprocessorUtils.getFilePath(result) + "\' is either not found or not a file", null); } } return result; } /** * Add a configuration file, it is a file which contains directives and global variable definitions * * @param file a file, it must not be null */ public void addConfigFile(@Nonnull final File file) { assertNotNull("File is null", file); configFiles.add(file); } /** * Get array of current registered configuration files * * @return a file array contains the current registered configuration files */ @Nonnull @MustNotContainNull public File[] getConfigFiles() { return configFiles.toArray(new File[configFiles.size()]); } /** * Generate new preprocessing state object, also the new preprocessing state will be saved as the current one in the context * * @param fileContainer a file container which will be using the preprocessor state, it must not be null * @param phaseIndex index of phase (0 - global, 1 - preprocessing) * @return new generated preprocessor state * @throws IOException it will be throws if there is any error in opening and reading operations */ @Nonnull public PreprocessingState produceNewPreprocessingState(@Nonnull final FileInfoContainer fileContainer, final int phaseIndex) throws IOException { assertNotNull("File container is null", fileContainer); if (verbose) { if (phaseIndex == 0) { logInfo("Start search global definitions in '" + PreprocessorUtils.getFilePath(fileContainer.getSourceFile()) + '\''); } else { logInfo("Start preprocessing '" + PreprocessorUtils.getFilePath(fileContainer.getSourceFile()) + '\''); } } this.currentState = new PreprocessingState(this, fileContainer, getInCharacterEncoding(), getOutCharacterEncoding(), this.compareDestination); return this.currentState; } /** * Generate new preprocessing state for a file container and a text container, also the new preprocessing state will be saved as the current one in the context * * @param fileContainer the file container to be used to create the new preprocessing state, it must not be null * @param textContainer the text container to be used to create the new preprocessing state, it must not be null * @return new generated preprocessing state */ @Nonnull public PreprocessingState produceNewPreprocessingState(@Nonnull final FileInfoContainer fileContainer, @Nonnull final TextFileDataContainer textContainer) { this.currentState = new PreprocessingState(this, fileContainer, textContainer, getInCharacterEncoding(), getOutCharacterEncoding(), this.compareDestination); return this.currentState; } /** * Get the last generated preprocessing state, it is the current one * * @return the last generated preprocessing state */ @Nonnull public PreprocessingState getPreprocessingState() { return this.currentState; } /** * Prepare exception with message and cause, or return cause if it is a preprocessor exception * * @param text the message text, must not be null * @param cause the cause, it can be null * @return prepared exception with additional information */ @Nonnull public PreprocessorException makeException(@Nonnull final String text, @Nullable final Throwable cause) { if (cause != null && cause instanceof PreprocessorException) { return (PreprocessorException) cause; } final FilePositionInfo[] includeStack; final String sourceLine; includeStack = this.currentState.makeIncludeStack(); sourceLine = this.currentState.getLastReadString(); return new PreprocessorException(text, sourceLine, includeStack, cause); } public void logForVerbose(@Nonnull final String str) { if (isVerbose()) { final String stack; stack = makeStackView(this.currentInCloneSource, this.cloned, this.currentState.getCurrentIncludeStack()); this.logInfo(str + (stack.isEmpty() ? ' ' : '\n') + stack); } } @Nonnull private static String makeStackView(@Nullable final TextFileDataContainer cloneSource, final boolean cloned, @Nullable @MustNotContainNull final List<TextFileDataContainer> list) { if (list == null || list.isEmpty()) { return ""; } final StringBuilder builder = new StringBuilder(); int tab = 5; for (int s = 0; s < tab; s++) { builder.append(' '); } builder.append('{'); if (cloned) { builder.append(cloneSource == null ? "*No src info" : "*" + cloneSource.getFile().getName() + ':' + cloneSource.getNextStringIndex()); } else { builder.append("File chain"); } builder.append('}'); tab += 5; int fileIndex = 1; for (int i = list.size() - 1; i >= 0; i--) { final TextFileDataContainer cur = list.get(i); builder.append('\n'); for (int s = 0; s < tab; s++) { builder.append(' '); } builder.append(">"); builder.append(fileIndex++).append(". "); builder.append(cur.getFile().getName()).append(':').append(cur.getLastReadStringIndex() + 1); tab += 3; } return builder.toString(); } }