/* $Id$ */ /** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 org.apache.manifoldcf.agents.transformation.forcedmetadata; import org.apache.manifoldcf.core.interfaces.*; import org.apache.manifoldcf.agents.interfaces.*; import java.io.*; import java.util.*; /** This connector works as a transformation connector, and merely adds specified metadata items. * */ public class ForcedMetadataConnector extends org.apache.manifoldcf.agents.transformation.BaseTransformationConnector { public static final String _rcsid = "@(#)$Id$"; // Nodes and attributes representing parameters and values. // There will be node for every parameter/value pair. public static final String NODE_EXPRESSION = "expression"; public static final String NODE_PAIR = "pair"; public static final String ATTRIBUTE_PARAMETER = "parameter"; public static final String NODE_FIELDMAP = "fieldmap"; public static final String NODE_KEEPMETADATA = "keepAllMetadata"; public static final String NODE_FILTEREMPTY = "filterEmpty"; public static final String ATTRIBUTE_SOURCE = "source"; public static final String ATTRIBUTE_TARGET = "target"; public static final String ATTRIBUTE_VALUE = "value"; // Templates private static final String VIEW_SPEC = "viewSpecification.html"; private static final String EDIT_SPEC_HEADER = "editSpecification.js"; private static final String EDIT_SPEC_EXPRESSIONS = "editSpecification_Expressions.html"; /** Get a pipeline version string, given a pipeline specification object. The version string is used to * uniquely describe the pertinent details of the specification and the configuration, to allow the Connector * Framework to determine whether a document will need to be processed again. * Note that the contents of any document cannot be considered by this method; only configuration and specification information * can be considered. * * This method presumes that the underlying connector object has been configured. *@param spec is the current pipeline specification object for this connection for the job that is doing the crawling. *@return a string, of unlimited length, which uniquely describes configuration and specification in such a way that * if two such strings are equal, nothing that affects how or whether the document is indexed will be different. */ @Override public VersionContext getPipelineDescription(Specification spec) throws ManifoldCFException, ServiceInterruption { SpecPacker sp = new SpecPacker(spec); return new VersionContext(sp.toPackedString(),params,spec); } /** Add (or replace) a document in the output data store using the connector. * This method presumes that the connector object has been configured, and it is thus able to communicate with the output data store should that be * necessary. * The OutputSpecification is *not* provided to this method, because the goal is consistency, and if output is done it must be consistent with the * output description, since that was what was partly used to determine if output should be taking place. So it may be necessary for this method to decode * an output description string in order to determine what should be done. *@param documentURI is the URI of the document. The URI is presumed to be the unique identifier which the output data store will use to process * and serve the document. This URI is constructed by the repository connector which fetches the document, and is thus universal across all output connectors. *@param outputDescription is the description string that was constructed for this document by the getOutputDescription() method. *@param document is the document data to be processed (handed to the output data store). *@param authorityNameString is the name of the authority responsible for authorizing any access tokens passed in with the repository document. May be null. *@param activities is the handle to an object that the implementer of a pipeline connector may use to perform operations, such as logging processing activity, * or sending a modified document to the next stage in the pipeline. *@return the document status (accepted or permanently rejected). *@throws IOException only if there's a stream error reading the document data. */ @Override public int addOrReplaceDocumentWithException(String documentURI, VersionContext pipelineDescription, RepositoryDocument document, String authorityNameString, IOutputAddActivity activities) throws ManifoldCFException, ServiceInterruption, IOException { // Unpack the forced metadata SpecPacker sp = new SpecPacker(pipelineDescription.getSpecification()); // Create a structure that will allow us access to fields without sharing Reader objects FieldDataFactory fdf = new FieldDataFactory(document); try { // We have to create a copy of the Repository Document, since we might be rearranging things RepositoryDocument docCopy = document.duplicate(); // We must explicitly copy all fields, since we can't share references to Reader objects and // expect anything to work right docCopy.clearFields(); // Clear fields, unless we're supposed to keep what we don't specify if (sp.filterEmpty()) { if (sp.keepAllMetadata()) { // Loop through fields and copy them, filtering empties Iterator<String> fields = document.getFields(); while (fields.hasNext()) { String field = fields.next(); moveData(docCopy,field,fdf,field,true); } } } else if (sp.keepAllMetadata()) { // Copy ALL current fields from old document, but go through FieldDataFactory Iterator<String> fields = document.getFields(); while (fields.hasNext()) { String field = fields.next(); moveData(docCopy,field,fdf,field,false); } } // Iterate through the expressions Iterator<String> expressionKeys = sp.getExpressionKeys(); while (expressionKeys.hasNext()) { String expressionKey = expressionKeys.next(); // Get the set of expressions for the key Set<String> values = sp.getExpressionValues(expressionKey); IDataSource[] dataSources = new IDataSource[values.size()]; int k = 0; for (String expression : values) { dataSources[k++] = processExpression(expression, fdf); } int totalSize = 0; for (IDataSource dataSource : dataSources) { if (dataSource != null) totalSize += dataSource.getSize(); } if (totalSize == 0) { docCopy.removeField(expressionKey); } else { // Each IDataSource will contribute zero or more results to the final array. But here's the tricky part: // the results all must be of the same type. If there are any differences, then we have to bash them all to // strings first. Object[] allValues; k = 0; if (allDates(dataSources)) { allValues = new Date[totalSize]; for (IDataSource dataSource : dataSources) { if (dataSource != null) { for (Object o : dataSource.getRawForm()) { allValues[k++] = o; } } } docCopy.addField(expressionKey,(Date[])conditionallyRemoveNulls(allValues,sp.filterEmpty())); } else if (allReaders(dataSources)) { if (sp.filterEmpty()) allValues = new String[totalSize]; else allValues = new Reader[totalSize]; for (IDataSource dataSource : dataSources) { if (dataSource != null) { Object[] sources = sp.filterEmpty()?dataSource.getStringForm():dataSource.getRawForm(); for (Object o : sources) { allValues[k++] = o; } } } if (sp.filterEmpty()) docCopy.addField(expressionKey,removeEmpties((String[])allValues)); else docCopy.addField(expressionKey,(Reader[])allValues); } else { allValues = new String[totalSize]; // Convert to strings throughout for (IDataSource dataSource : dataSources) { if (dataSource != null) { for (Object o : dataSource.getStringForm()) { allValues[k++] = o; } } } if (sp.filterEmpty()) docCopy.addField(expressionKey,removeEmpties((String[])allValues)); else docCopy.addField(expressionKey,(String[])allValues); } } } // Finally, send the modified repository document onward to the next pipeline stage. // If we'd done anything to the stream, we'd have needed to create a new RepositoryDocument object and copied the // data into it, and closed the new stream after sendDocument() was called. return activities.sendDocument(documentURI,docCopy); } finally { fdf.close(); } } protected static boolean allDates(IDataSource[] dataSources) throws IOException, ManifoldCFException { for (IDataSource ds : dataSources) { if (ds != null && !(ds.getRawForm() instanceof Date[])) return false; } return true; } protected static boolean allReaders(IDataSource[] dataSources) throws IOException, ManifoldCFException { for (IDataSource ds : dataSources) { if (ds != null && !(ds.getRawForm() instanceof Reader[])) return false; } return true; } protected static void moveData(RepositoryDocument docCopy, String target, FieldDataFactory document, String field, boolean filterEmpty) throws ManifoldCFException, IOException { Object[] fieldData = document.getField(field); if (fieldData instanceof Date[]) docCopy.addField(target,(Date[])conditionallyRemoveNulls(fieldData,filterEmpty)); else if (fieldData instanceof Reader[]) { // To strip out empty fields, we will need to convert readers to strings if (filterEmpty) docCopy.addField(target,removeEmpties(document.getFieldAsStrings(field))); else docCopy.addField(target,(Reader[])fieldData); } else if (fieldData instanceof String[]) { String[] processedFieldData; if (filterEmpty) processedFieldData = removeEmpties((String[])fieldData); else processedFieldData = (String[])fieldData; docCopy.addField(target,processedFieldData); } } protected static String[] removeEmpties(String[] input) { int count = 0; for (String s : input) { if (s != null && s.length() > 0) count++; } if (count == input.length) return input; String[] rval = new String[count]; count = 0; for (String s : input) { if (s != null && s.length() > 0) rval[count++] = s; } return rval; } protected static Object[] conditionallyRemoveNulls(Object[] input, boolean filterEmpty) { if (!filterEmpty) return input; int count = 0; for (Object o : input) { if (o != null) count++; } if (count == input.length) return input; Object[] rval = new Object[count]; count = 0; for (Object o : input) { if (o != null) rval[count++] = o; } return rval; } // UI support methods. // // These support methods come in two varieties. The first bunch (inherited from IConnector) is involved in setting up connection configuration information. // The second bunch // is involved in presenting and editing pipeline specification information for a connection within a job. The two kinds of methods are accordingly treated differently, // in that the first bunch cannot assume that the current connector object is connected, while the second bunch can. That is why the first bunch // receives a thread context argument for all UI methods, while the second bunch does not need one (since it has already been applied via the connect() // method, above). /** Obtain the name of the form check javascript method to call. *@param connectionSequenceNumber is the unique number of this connection within the job. *@return the name of the form check javascript method. */ @Override public String getFormCheckJavascriptMethodName(int connectionSequenceNumber) { return "s"+connectionSequenceNumber+"_checkSpecification"; } /** Obtain the name of the form presave check javascript method to call. *@param connectionSequenceNumber is the unique number of this connection within the job. *@return the name of the form presave check javascript method. */ @Override public String getFormPresaveCheckJavascriptMethodName(int connectionSequenceNumber) { return "s"+connectionSequenceNumber+"_checkSpecificationForSave"; } /** Output the specification header section. * This method is called in the head section of a job page which has selected a pipeline connection of the current type. Its purpose is to add the required tabs * to the list, and to output any javascript methods that might be needed by the job editing HTML. *@param out is the output to which any HTML should be sent. *@param locale is the preferred local of the output. *@param os is the current pipeline specification for this connection. *@param connectionSequenceNumber is the unique number of this connection within the job. *@param tabsArray is an array of tab names. Add to this array any tab names that are specific to the connector. */ @Override public void outputSpecificationHeader(IHTTPOutput out, Locale locale, Specification os, int connectionSequenceNumber, List<String> tabsArray) throws ManifoldCFException, IOException { // Output specification header tabsArray.add(Messages.getString(locale, "ForcedMetadata.Expressions")); Map<String, Object> paramMap = new HashMap<String, Object>(); paramMap.put("SEQNUM",Integer.toString(connectionSequenceNumber)); Messages.outputResourceWithVelocity(out,locale,EDIT_SPEC_HEADER,paramMap); } /** Output the specification body section. * This method is called in the body section of a job page which has selected a pipeline connection of the current type. Its purpose is to present the required form elements for editing. * The coder can presume that the HTML that is output from this configuration will be within appropriate <html>, <body>, and <form> tags. The name of the * form is "editjob". *@param out is the output to which any HTML should be sent. *@param locale is the preferred local of the output. *@param os is the current pipeline specification for this job. *@param connectionSequenceNumber is the unique number of this connection within the job. *@param actualSequenceNumber is the connection within the job that has currently been selected. *@param tabName is the current tab name. */ @Override public void outputSpecificationBody(IHTTPOutput out, Locale locale, Specification os, int connectionSequenceNumber, int actualSequenceNumber, String tabName) throws ManifoldCFException, IOException { // Output specification body Map<String, Object> paramMap = new HashMap<String, Object>(); paramMap.put("TABNAME", tabName); paramMap.put("SEQNUM",Integer.toString(connectionSequenceNumber)); paramMap.put("SELECTEDNUM",Integer.toString(actualSequenceNumber)); fillInExpressionsTab(paramMap, os); Messages.outputResourceWithVelocity(out,locale,EDIT_SPEC_EXPRESSIONS,paramMap); } /** Process a specification post. * This method is called at the start of job's edit or view page, whenever there is a possibility that form data for a connection has been * posted. Its purpose is to gather form information and modify the transformation specification accordingly. * The name of the posted form is "editjob". *@param variableContext contains the post data, including binary file-upload information. *@param locale is the preferred local of the output. *@param os is the current pipeline specification for this job. *@param connectionSequenceNumber is the unique number of this connection within the job. *@return null if all is well, or a string error message if there is an error that should prevent saving of the job (and cause a redirection to an error page). */ @Override public String processSpecificationPost(IPostParameters variableContext, Locale locale, Specification os, int connectionSequenceNumber) throws ManifoldCFException { // Process specification post String seqPrefix = "s"+connectionSequenceNumber+"_"; String expressionCount = variableContext.getParameter(seqPrefix+"expression_count"); if (expressionCount != null) { int count = Integer.parseInt(expressionCount); // Delete old spec data, including legacy node types we no longer use int i = 0; while (i < os.getChildCount()) { SpecificationNode cn = os.getChild(i); if (cn.getType().equals(NODE_EXPRESSION) || cn.getType().equals(NODE_PAIR) || cn.getType().equals(NODE_FIELDMAP)) os.removeChild(i); else i++; } // Now, go through form data for (int j = 0; j < count; j++) { String op = variableContext.getParameter(seqPrefix+"expression_"+j+"_op"); if (op != null && op.equals("Delete")) continue; String paramName = variableContext.getParameter(seqPrefix+"expression_"+j+"_name"); String paramRemove = variableContext.getParameter(seqPrefix+"expression_"+j+"_remove"); String paramValue = variableContext.getParameter(seqPrefix+"expression_"+j+"_value"); SpecificationNode sn = new SpecificationNode(NODE_EXPRESSION); sn.setAttribute(ATTRIBUTE_PARAMETER,paramName); if (!(paramRemove != null && paramRemove.equals("true"))) sn.setAttribute(ATTRIBUTE_VALUE,paramValue); os.addChild(os.getChildCount(),sn); } // Look for add operation String addOp = variableContext.getParameter(seqPrefix+"expression_op"); if (addOp != null && addOp.equals("Add")) { String paramName = variableContext.getParameter(seqPrefix+"expression_name"); String paramRemove = variableContext.getParameter(seqPrefix+"expression_remove"); String paramValue = variableContext.getParameter(seqPrefix+"expression_value"); SpecificationNode sn = new SpecificationNode(NODE_EXPRESSION); sn.setAttribute(ATTRIBUTE_PARAMETER,paramName); if (!(paramRemove != null && paramRemove.equals("true"))) sn.setAttribute(ATTRIBUTE_VALUE,paramValue); os.addChild(os.getChildCount(),sn); } } String x = variableContext.getParameter(seqPrefix+"keepallmetadata_present"); if (x != null && x.length() > 0) { String keepAll = variableContext.getParameter(seqPrefix+"keepallmetadata"); if (keepAll == null) keepAll = "false"; // About to gather the fieldmapping nodes, so get rid of the old ones. int i = 0; while (i < os.getChildCount()) { SpecificationNode node = os.getChild(i); if (node.getType().equals(NODE_KEEPMETADATA)) os.removeChild(i); else i++; } // Gather the keep all metadata parameter to be the last one SpecificationNode node = new SpecificationNode(NODE_KEEPMETADATA); node.setAttribute(ATTRIBUTE_VALUE, keepAll); // Add the new keepallmetadata config parameter os.addChild(os.getChildCount(), node); } x = variableContext.getParameter(seqPrefix+"filterempty_present"); if (x != null && x.length() > 0) { String filterEmpty = variableContext.getParameter(seqPrefix+"filterempty"); if (filterEmpty == null) filterEmpty = "false"; // About to gather the fieldmapping nodes, so get rid of the old ones. int i = 0; while (i < os.getChildCount()) { SpecificationNode node = os.getChild(i); if (node.getType().equals(NODE_FILTEREMPTY)) os.removeChild(i); else i++; } // Gather the keep all metadata parameter to be the last one SpecificationNode node = new SpecificationNode(NODE_FILTEREMPTY); node.setAttribute(ATTRIBUTE_VALUE, filterEmpty); // Add the new keepallmetadata config parameter os.addChild(os.getChildCount(), node); } return null; } /** View specification. * This method is called in the body section of a job's view page. Its purpose is to present the pipeline specification information to the user. * The coder can presume that the HTML that is output from this configuration will be within appropriate <html> and <body> tags. *@param out is the output to which any HTML should be sent. *@param locale is the preferred local of the output. *@param connectionSequenceNumber is the unique number of this connection within the job. *@param os is the current pipeline specification for this job. */ @Override public void viewSpecification(IHTTPOutput out, Locale locale, Specification os, int connectionSequenceNumber) throws ManifoldCFException, IOException { // View specification Map<String, Object> paramMap = new HashMap<String, Object>(); paramMap.put("SEQNUM",Integer.toString(connectionSequenceNumber)); // Fill in the map with data from all tabs fillInExpressionsTab(paramMap, os); Messages.outputResourceWithVelocity(out,locale,VIEW_SPEC,paramMap); } protected static void fillInExpressionsTab(Map<String,Object> paramMap, Specification os) { final Map<String,Set<String>> expressions = new HashMap<String,Set<String>>(); final Map<String,Set<String>> expressionAdditions = new HashMap<String,Set<String>>(); final Map<String,Set<String>> additions = new HashMap<String,Set<String>>(); String keepAllMetadataValue = "true"; String filterEmptyValue = "true"; for (int i = 0; i < os.getChildCount(); i++) { SpecificationNode sn = os.getChild(i); if (sn.getType().equals(NODE_FIELDMAP)) { String source = sn.getAttributeValue(ATTRIBUTE_SOURCE); String target = sn.getAttributeValue(ATTRIBUTE_TARGET); String targetDisplay; expressions.put(source,new HashSet<String>()); if (target != null) { Set<String> sources = new HashSet<String>(); sources.add("${"+source+"}"); expressions.put(target,sources); } } else if (sn.getType().equals(NODE_PAIR)) { String parameter = sn.getAttributeValue(ATTRIBUTE_PARAMETER); String value = sn.getAttributeValue(ATTRIBUTE_VALUE); // Since the same target is completely superceded by a NODE_PAIR, but NODE_PAIRs // are cumulative, I have to build these completely and then post-process them. Set<String> addition = additions.get(parameter); if (addition == null) { addition = new HashSet<String>(); additions.put(parameter,addition); } addition.add(nonExpressionEscape(value)); } else if (sn.getType().equals(NODE_EXPRESSION)) { String parameter = sn.getAttributeValue(ATTRIBUTE_PARAMETER); String value = sn.getAttributeValue(ATTRIBUTE_VALUE); if (value == null) { expressionAdditions.put(parameter,new HashSet<String>()); } else { Set<String> expressionAddition = expressionAdditions.get(parameter); if (expressionAddition == null) { expressionAddition = new HashSet<String>(); expressionAdditions.put(parameter,expressionAddition); } expressionAddition.add(value); } } else if (sn.getType().equals(NODE_KEEPMETADATA)) { keepAllMetadataValue = sn.getAttributeValue(ATTRIBUTE_VALUE); } else if (sn.getType().equals(NODE_FILTEREMPTY)) { filterEmptyValue = sn.getAttributeValue(ATTRIBUTE_VALUE); } } // Postprocessing. // Override the moves with the additions for (String parameter : additions.keySet()) { expressions.put(parameter,additions.get(parameter)); } // Override all with expression additions for (String parameter : expressionAdditions.keySet()) { expressions.put(parameter,expressionAdditions.get(parameter)); } // Problem: how to display case where we want a null source?? // A: Special value List<Map<String,String>> pObject = new ArrayList<Map<String,String>>(); String[] keys = expressions.keySet().toArray(new String[0]); java.util.Arrays.sort(keys); // Now, build map for (String key : keys) { Set<String> values = expressions.get(key); if (values.size() == 0) { Map<String,String> record = new HashMap<String,String>(); record.put("parameter",key); record.put("value",""); record.put("isnull","true"); pObject.add(record); } else { String[] valueArray = values.toArray(new String[0]); java.util.Arrays.sort(valueArray); for (String value : valueArray) { Map<String,String> record = new HashMap<String,String>(); record.put("parameter",key); record.put("value",value); record.put("isnull","false"); pObject.add(record); } } } paramMap.put("EXPRESSIONS",pObject); paramMap.put("KEEPALLMETADATA",keepAllMetadataValue); paramMap.put("FILTEREMPTY",filterEmptyValue); } /** This is used to upgrade older constant values to new ones, that won't trigger expression eval. */ protected static String nonExpressionEscape(String input) { // Not doing any escaping yet return input; } /** This is used to unescape text that's been escaped to prevent substitution of ${} expressions. */ protected static String nonExpressionUnescape(String input) { // Not doing any escaping yet return input; } protected static IDataSource append(IDataSource currentValues, IDataSource data) throws IOException, ManifoldCFException { // currentValues and data can either be: // Date[], String[], or Reader[]. // We want to preserve the type in as high a form as possible when we compute the combinations. if (currentValues == null) return data; if (currentValues.getSize() == 0) return currentValues; // Any combination causes conversion to a string, so if we get here, we can read the inputs all // as strings safely. String[] currentStrings = currentValues.getStringForm(); String[] dataStrings = data.getStringForm(); String[] rval = new String[currentStrings.length * dataStrings.length]; int rvalIndex = 0; for (String currentString : currentStrings) { for (String dataString : dataStrings) { rval[rvalIndex++] = currentString + dataString; } } return new StringSource(rval); } public static IDataSource processExpression(String expression, FieldDataFactory sourceDocument) throws IOException, ManifoldCFException { int index = 0; IDataSource input = null; while (true) { // If we're at the end, return the input if (index == expression.length()) return input; // Look for next field specification int field = expression.indexOf("${",index); if (field == -1) return append(input, new StringSource(nonExpressionUnescape(expression.substring(index)))); if (field > 0) input = append(input, new StringSource(nonExpressionUnescape(expression.substring(index,field)))); // Parse the field name, and regular expression (if any) StringBuilder fieldNameBuffer = new StringBuilder(); StringBuilder regExpBuffer = new StringBuilder(); StringBuilder groupNumberBuffer = new StringBuilder(); field = parseArgument(expression, field+2, fieldNameBuffer); field = parseArgument(expression, field, regExpBuffer); field = parseArgument(expression, field, groupNumberBuffer); int fieldEnd = parseToEnd(expression, field); if (fieldEnd == expression.length()) { if (fieldNameBuffer.length() > 0) return append(input, new FieldSource(sourceDocument, fieldNameBuffer.toString(), regExpBuffer.toString(), groupNumberBuffer.toString())); return input; } else { if (fieldNameBuffer.length() > 0) input = append(input, new FieldSource(sourceDocument, fieldNameBuffer.toString(), regExpBuffer.toString(), groupNumberBuffer.toString())); index = fieldEnd; } } } protected static int parseArgument(final String input, int start, final StringBuilder output) { // Parse until we hit the end marker or an unescaped pipe symbol while (true) { if (input.length() == start) return start; char theChar = input.charAt(start); if (theChar == '}') return start; start++; if (theChar == '|') return start; if (theChar == '\\') { if (input.length() == start) return start; theChar = input.charAt(start++); } output.append(theChar); } } protected static int parseToEnd(final String input, int start) { while (true) { if (input.length() == start) return start; char theChar = input.charAt(start++); if (theChar == '}') return start; if (theChar == '\\') { if (input.length() == start) return start; start++; } } } protected static class SpecPacker { private final boolean keepAllMetadata; private final boolean filterEmpty; private final Map<String,Set<String>> expressions = new HashMap<String,Set<String>>(); public SpecPacker(Specification os) { boolean keepAllMetadata = true; boolean filterEmpty = true; final Map<String,Set<String>> additions = new HashMap<String,Set<String>>(); final Map<String,Set<String>> expressionAdditions = new HashMap<String,Set<String>>(); for (int i = 0; i < os.getChildCount(); i++) { SpecificationNode sn = os.getChild(i); if(sn.getType().equals(NODE_KEEPMETADATA)) { String value = sn.getAttributeValue(ATTRIBUTE_VALUE); keepAllMetadata = Boolean.parseBoolean(value); } else if (sn.getType().equals(NODE_FILTEREMPTY)) { String value = sn.getAttributeValue(ATTRIBUTE_VALUE); filterEmpty = Boolean.parseBoolean(value); } else if (sn.getType().equals(NODE_FIELDMAP)) { String source = sn.getAttributeValue(ATTRIBUTE_SOURCE); String target = sn.getAttributeValue(ATTRIBUTE_TARGET); expressions.put(source,new HashSet<String>()); // Null target means to remove the *source* from the document. if (target != null) { Set<String> sources = new HashSet<String>(); sources.add("${"+source+"}"); expressions.put(target,sources); } } else if (sn.getType().equals(NODE_PAIR)) { String parameter = sn.getAttributeValue(ATTRIBUTE_PARAMETER); String value = sn.getAttributeValue(ATTRIBUTE_VALUE); // Since the same target is completely superceded by a NODE_PAIR, but NODE_PAIRs // are cumulative, I have to build these completely and then post-process them. Set<String> addition = additions.get(parameter); if (addition == null) { addition = new HashSet<String>(); additions.put(parameter,addition); } addition.add(nonExpressionEscape(value)); } else if (sn.getType().equals(NODE_EXPRESSION)) { String parameter = sn.getAttributeValue(ATTRIBUTE_PARAMETER); String value = sn.getAttributeValue(ATTRIBUTE_VALUE); if (value == null) { expressionAdditions.put(parameter,new HashSet<String>()); } else { Set<String> expressionAddition = expressionAdditions.get(parameter); if (expressionAddition == null) { expressionAddition = new HashSet<String>(); expressionAdditions.put(parameter,expressionAddition); } expressionAddition.add(value); } } } // Override the moves with the additions for (String parameter : additions.keySet()) { expressions.put(parameter,additions.get(parameter)); } // Override all with expression additions for (String parameter : expressionAdditions.keySet()) { expressions.put(parameter,expressionAdditions.get(parameter)); } this.keepAllMetadata = keepAllMetadata; this.filterEmpty = filterEmpty; } public String toPackedString() { StringBuilder sb = new StringBuilder(); int i; final String[] sortArray = expressions.keySet().toArray(new String[0]); java.util.Arrays.sort(sortArray); // Pack the list of keys packList(sb,sortArray,'+'); for (String key : sortArray) { Set<String> values = expressions.get(key); String[] valueArray = values.toArray(new String[0]); java.util.Arrays.sort(valueArray); packList(sb,valueArray,'+'); } // Keep all metadata if (keepAllMetadata) sb.append('+'); else sb.append('-'); // Filter empty if (filterEmpty) sb.append('+'); else sb.append('-'); return sb.toString(); } public Iterator<String> getExpressionKeys() { return expressions.keySet().iterator(); } public Set<String> getExpressionValues(String key) { return expressions.get(key); } public boolean keepAllMetadata() { return keepAllMetadata; } public boolean filterEmpty() { return filterEmpty; } } }