Coverage Summary for Class: VariableReplacer (org.kitodo.production.helper)
Class |
Method, %
|
Line, %
|
VariableReplacer |
75%
(15/20)
|
52,7%
(87/165)
|
VariableReplacer$1 |
0%
(0/1)
|
0%
(0/1)
|
VariableReplacer$MetadataLevel |
0%
(0/1)
|
0%
(0/4)
|
Total |
68,2%
(15/22)
|
51,2%
(87/170)
|
/*
* (c) Kitodo. Key to digital objects e. V. <contact@kitodo.org>
*
* This file is part of the Kitodo project.
*
* It is licensed under GNU General Public License version 3 or later.
*
* For the full copyright and license information, please read the
* GPL3-License.txt file that was distributed with this source code.
*/
package org.kitodo.production.helper;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.io.FilenameUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.kitodo.api.dataformat.LogicalDivision;
import org.kitodo.api.dataformat.Workpiece;
import org.kitodo.config.ConfigCore;
import org.kitodo.config.KitodoConfig;
import org.kitodo.config.enums.ParameterCore;
import org.kitodo.data.database.beans.Process;
import org.kitodo.data.database.beans.Task;
import org.kitodo.production.metadata.MetadataEditor;
/**
* Replaces placeholders in a string. The variable replacer enables placeholders
* specified in a character string to be replaced with internal values from the
* process or the metadata of its workpiece. The character string is, for
* example, the call of an external program.
*/
public class VariableReplacer {
private static final Logger logger = LogManager.getLogger(VariableReplacer.class);
/**
* There are three different levels of access to the workpiece's metadata.
*/
private enum MetadataLevel {
/**
* First, a metadata entry with the specified name is searched for in
* the first division of the first subordinate hierarchy level; if it is
* not found there, a search is then made in the top hierarchy level.
*/
ALL,
/**
* A metadata entry with the specified name is only searched for in the
* first division of the first subordinate hierarchy level.
*/
FIRSTCHILD,
/**
* A metadata entry with the specified name is only searched for in the
* top hierarchy level.
*/
TOPSTRUCT
}
/**
* This regular expression is used to search for placeholders that need to
* be replaced.
*/
private static final Pattern VARIABLE_FINDER_REGEX = Pattern.compile(
"(\\$?)\\((?:(prefs|processid|processtitle|projectid|stepid|stepname|generatorsource|generatorsourcepath)|"
+ "(?:(meta|process|product|template)\\.(?:(firstchild|topstruct)\\.)?([^)]+)|"
+ "(?:(filename|basename|relativepath))))\\)");
/**
* The map is filled with replacement instructions that are required for
* backwards compatibility with version 2.
*/
private static Map<String, String> legacyVariablesMap;
/**
* The program builds a regular expression to search for outdated
* replacement instructions and to replace them with the new instructions in
* a first step.
*/
private static Pattern legacyVariablesPattern;
private Workpiece workpiece;
private Process process;
private Task task;
/**
* Creates a new Variable Replacer.
*
* @param workpiece
* Workpiece to read metadata values from
* @param process
* Process to read values from
* @param task
* Task to read values from
*/
public VariableReplacer(Workpiece workpiece, Process process, Task task) {
initializeLegacyVariablesPreprocessor();
this.workpiece = workpiece;
this.process = process;
this.task = task;
}
/**
* The method is called by the constructor of the class to load conversion
* instructions for obsolete replacement patterns from the configuration
* file.
*/
private final void initializeLegacyVariablesPreprocessor() {
StringBuilder regexBuilder = null;
boolean useLegacyVariablesPreprocessor = false;
for (Iterator<String> iterator = ConfigCore.getConfig().getKeys(); iterator.hasNext();) {
String key = iterator.next();
if (key.startsWith("variable.")) {
String variableName = key.substring(9);
if (useLegacyVariablesPreprocessor) {
regexBuilder.append('|');
} else {
regexBuilder = new StringBuilder("\\((");
legacyVariablesMap = new HashMap<>();
useLegacyVariablesPreprocessor = true;
}
regexBuilder.append(Pattern.quote(variableName));
legacyVariablesMap.put(variableName, ConfigCore.getParameter(key));
}
}
if (useLegacyVariablesPreprocessor) {
regexBuilder.append(")\\)");
legacyVariablesPattern = Pattern.compile(regexBuilder.toString());
}
}
/**
* Replace variables within a string. Like an ant, run through the variables
* and fetch them from the digital document.
*
* @param stringWithVariables
* a string maybe holding variables
* @return string with variables replaced
*/
public String replace(String stringWithVariables) {
return replaceWithFilename(stringWithVariables,null);
}
/**
* Replace variables withing a string. Like an ant, run through the variables
* and fetch them from the digital document. Filename variables are replaced using the filename parameter.
*
* @param stringWithVariables
* string with placeholders
* @param filename
* filename for replacement
*
* @return string with replaced placeholders
*/
public String replaceWithFilename(String stringWithVariables, String filename) {
if (Objects.isNull(stringWithVariables)) {
return "";
}
stringWithVariables = invokeLegacyVariableReplacer(stringWithVariables);
Matcher variableFinder = VARIABLE_FINDER_REGEX.matcher(stringWithVariables);
boolean stringChanged = false;
StringBuffer replacedStringBuffer = null;
while (variableFinder.find()) {
if (!stringChanged) {
replacedStringBuffer = new StringBuffer();
stringChanged = true;
}
variableFinder.appendReplacement(replacedStringBuffer, determineReplacement(variableFinder, filename));
}
if (stringChanged) {
variableFinder.appendTail(replacedStringBuffer);
return replacedStringBuffer.toString();
} else {
return stringWithVariables;
}
}
/**
* Replaces outdated replacement patterns with appropriate ones.
*
* @param stringToReplace
* string (perhaps) containing obsolete replacement patterns
* @return string in which obsolete replacement patterns have been replaced
* by appropriate ones
*/
private String invokeLegacyVariableReplacer(String stringToReplace) {
if (Objects.isNull(legacyVariablesPattern)) {
return stringToReplace;
}
Matcher legacyVariablesMatcher = legacyVariablesPattern.matcher(stringToReplace);
StringBuffer replacedLegaycVariablesBuffer = new StringBuffer();
while (legacyVariablesMatcher.find()) {
legacyVariablesMatcher.appendReplacement(replacedLegaycVariablesBuffer,
legacyVariablesMap.get(legacyVariablesMatcher.group(1)));
}
legacyVariablesMatcher.appendTail(replacedLegaycVariablesBuffer);
return replacedLegaycVariablesBuffer.toString();
}
/**
* This method is called in the replacement loop to determine the
* replacement value for a revealed variable.
*/
private String determineReplacement(Matcher variableFinder, String filename) {
if (Objects.nonNull(variableFinder.group(2))) {
return determineReplacementForInternalValue(variableFinder);
}
if (Objects.nonNull(variableFinder.group(3))) {
return determineReplacementForMetadata(variableFinder);
}
if (Objects.nonNull(variableFinder.group(6)) && Objects.nonNull(filename)) {
return determineReplacementForFilePlaceholder(variableFinder, filename);
}
return variableFinder.group();
}
/**
* If an internal value is to be determined, it is determined here.
*/
private String determineReplacementForInternalValue(Matcher variableFinder) {
switch (variableFinder.group(2)) {
case "prefs":
return determineReplacementForPrefs(variableFinder);
case "processid":
return determineReplacementForProcessid(variableFinder);
case "processtitle":
return determineReplacementForProcesstitle(variableFinder);
case "projectid":
return determineReplacementForProjectid(variableFinder);
case "stepid":
return determineReplacementForStepid(variableFinder);
case "stepname":
return determineReplacementForStepname(variableFinder);
case "generatorsource" :
case "generatorsourcepath":
return determineReplacementForGeneratorSource(variableFinder, variableFinder.group(2));
default:
logger.warn("Cannot replace \"{}\": no such case defined in switch", variableFinder.group());
return variableFinder.group();
}
}
private String determineReplacementForPrefs(Matcher variableFinder) {
String rulesetsDirectory;
try {
rulesetsDirectory = ConfigCore.getParameter(ParameterCore.DIR_RULESETS);
} catch (NoSuchElementException e) {
logger.warn("Cannot replace \"(prefs)\": Missing configuration entry: directory.rulesets");
return variableFinder.group(1);
}
if (Objects.isNull(process)) {
logger.warn("Cannot replace \"(prefs)\": no process given");
return variableFinder.group(1);
}
if (Objects.isNull(process.getRuleset())) {
logger.warn("Cannot replace \"(prefs)\": process has no ruleset assigned");
return variableFinder.group(1);
}
if (Objects.isNull(process.getRuleset().getFile())) {
logger.warn("Cannot replace \"(prefs)\": process's ruleset has no file");
return variableFinder.group(1);
}
return variableFinder.group(1) + rulesetsDirectory + process.getRuleset().getFile();
}
private String determineReplacementForProcessid(Matcher variableFinder) {
if (Objects.isNull(process)) {
logger.warn("Cannot replace \"(processid)\": no process given");
return variableFinder.group(1);
}
return variableFinder.group(1) + process.getId().toString();
}
private String determineReplacementForProcesstitle(Matcher variableFinder) {
if (Objects.isNull(process)) {
logger.warn("Cannot replace \"(processtitle)\": no process given");
return variableFinder.group(1);
}
return variableFinder.group(1) + process.getTitle();
}
private String determineReplacementForProjectid(Matcher variableFinder) {
if (Objects.isNull(process)) {
logger.warn("Cannot replace \"(projectid)\": no process given");
return variableFinder.group(1);
}
if (Objects.isNull(process.getProject())) {
logger.warn("Cannot replace \"(projectid)\": process has no project assigned");
return variableFinder.group(1);
}
return variableFinder.group(1) + String.valueOf(process.getProject().getId());
}
private String determineReplacementForStepid(Matcher variableFinder) {
if (Objects.isNull(task)) {
logger.warn("Cannot replace \"(stepid)\": no task given");
return variableFinder.group(1);
}
return variableFinder.group(1) + String.valueOf(task.getId());
}
private String determineReplacementForStepname(Matcher variableFinder) {
if (Objects.isNull(task)) {
logger.warn("Cannot replace \"(stepname)\": no task given");
return variableFinder.group(1);
}
return variableFinder.group(1) + task.getTitle();
}
private String determineReplacementForGeneratorSource(Matcher variableFinder, String match) {
if (Objects.isNull(process)) {
logger.warn("Cannot replace \"(" + match + ")\": no process given");
return variableFinder.group(1);
}
if (Objects.isNull(process.getProject())) {
logger.warn("Cannot replace \"(" + match + ")\": process has no project assigned");
return variableFinder.group(1);
}
if (Objects.isNull(process.getProject().getGeneratorSource())) {
logger.warn("Cannot replace \"(" + match + ")\": process has no generator source assigned");
return variableFinder.group(1);
}
//Since image paths may contain variables themselves, use recursion
String generatorSource = replace(String.valueOf(process.getProject().getGeneratorSource().getPath()));
String replacedString = variableFinder.group(1);
if (match.equals("generatorsource")) {
replacedString += generatorSource;
}
else if (match.equals("generatorsourcepath")) {
replacedString += KitodoConfig.getKitodoDataDirectory() + process.getId() + "/" + generatorSource;
}
return replacedString;
}
/**
* If a value is to be determined from the metadata, it is determined here.
*/
private String determineReplacementForMetadata(Matcher variableFinder) {
String dollarSignIfToKeep = variableFinder.group(1);
MetadataLevel metadataLevel;
if (variableFinder.group(3).equals("meta")) {
if (dollarSignIfToKeep.isEmpty()) {
return variableFinder.group();
} else {
dollarSignIfToKeep = "";
}
metadataLevel = Objects.isNull(variableFinder.group(4)) ? MetadataLevel.ALL
: MetadataLevel.valueOf(variableFinder.group(4).toUpperCase());
} else {
metadataLevel = MetadataLevel.TOPSTRUCT;
}
if (Objects.isNull(workpiece)) {
logger.warn("Cannot replace \"{}\": no workpiece given", variableFinder.group());
return dollarSignIfToKeep;
}
switch (metadataLevel) {
case ALL:
List<LogicalDivision> allChildren = workpiece.getLogicalStructure().getChildren();
String allFirstchildValue = allChildren.isEmpty() ? null
: MetadataEditor.getMetadataValue(allChildren.get(0), variableFinder.group(5));
if (Objects.nonNull(allFirstchildValue)) {
return allFirstchildValue;
}
// else fall through
case TOPSTRUCT:
return determineReplacementForTopstruct(variableFinder, dollarSignIfToKeep);
case FIRSTCHILD:
return determineReplacementForFirstchild(variableFinder, dollarSignIfToKeep);
default:
throw new IllegalStateException("complete switch");
}
}
private String determineReplacementForTopstruct(Matcher variableFinder, String failureResult) {
String value = MetadataEditor.getMetadataValue(workpiece.getLogicalStructure(), variableFinder.group(5));
if (Objects.isNull(value)) {
logger.warn("Cannot replace \"{}\": No such metadata entry in the root element", variableFinder.group());
return failureResult;
}
return value;
}
private String determineReplacementForFirstchild(Matcher variableFinder, String failureResult) {
List<LogicalDivision> firstchildChildren = workpiece.getLogicalStructure().getChildren();
if (firstchildChildren.isEmpty()) {
logger.warn("Cannot replace \"{}\": Workpiece doesn't have subordinate logical divisions",
variableFinder.group());
return failureResult;
}
String value = MetadataEditor.getMetadataValue(firstchildChildren.get(0), variableFinder.group(5));
if (Objects.isNull(value)) {
logger.warn("Cannot replace \"{}\": No such metadata entry in the first division", variableFinder.group());
return failureResult;
}
return value;
}
/**
* Checks whether a string contains file variables.
*
* @param stringWithVariables
* string to be checked for file variables
* @return true if string contains file variables
*/
public boolean containsFiles(String stringWithVariables) {
if (Objects.isNull(stringWithVariables)) {
return false;
}
return stringWithVariables.contains("(filename)") | stringWithVariables.contains("(basename)")
| stringWithVariables.contains("(relativepath)");
}
/**
* If a filename is to be determined, it is determined here.
*/
private String determineReplacementForFilePlaceholder(Matcher variableFinder, String filename) {
switch (variableFinder.group(6)) {
case "filename":
return variableFinder.group(1) + FilenameUtils.getName(filename);
case "basename":
return variableFinder.group(1) + FilenameUtils.getBaseName(filename);
case "relativepath":
return variableFinder.group(1) + filename;
default:
logger.warn("Cannot replace \"{}\": no such case defined in switch", variableFinder.group());
return variableFinder.group();
}
}
}