001/* 002 * Copyright 2011 Atteo. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 005 * in compliance with the License. You may obtain a copy of the License at 006 * 007 * http://www.apache.org/licenses/LICENSE-2.0 008 * 009 * Unless required by applicable law or agreed to in writing, software distributed under the License 010 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 011 * or implied. See the License for the specific language governing permissions and limitations under 012 * the License. 013 */ 014package org.atteo.evo.filtering; 015 016import java.util.ArrayList; 017import java.util.HashSet; 018import java.util.List; 019import java.util.Properties; 020import java.util.Set; 021 022import org.slf4j.Logger; 023import org.slf4j.LoggerFactory; 024import org.w3c.dom.Attr; 025import org.w3c.dom.Element; 026import org.w3c.dom.NamedNodeMap; 027import org.w3c.dom.Node; 028import org.w3c.dom.NodeList; 029import org.w3c.dom.Text; 030 031/** 032 * Properties filtering engine. 033 */ 034public class Filtering { 035 private final static Logger logger = LoggerFactory.getLogger(Filtering.class); 036 037 private final static class LoopCheckerResolver implements PropertyResolver { 038 private Set<String> inProgress = new HashSet<String>(); 039 private PropertyResolver resolver; 040 041 private LoopCheckerResolver(PropertyResolver resolver) { 042 this.resolver = resolver; 043 } 044 045 @Override 046 public String resolveProperty(String name, PropertyResolver ignore) throws PropertyNotFoundException { 047 if (inProgress.contains(name)) { 048 throw new CircularPropertyResolutionException(name); 049 } 050 logger.debug("Resolving property: " + name); 051 inProgress.add(name); 052 053 String value = resolver.resolveProperty(name, this); 054 055 inProgress.remove(name); 056 057 return value; 058 } 059 } 060 061 public static final class Token { 062 private String value; 063 private boolean property; 064 065 public Token(String value, boolean property) { 066 this.value = value; 067 this.property = property; 068 } 069 070 public String getValue() { 071 return value; 072 } 073 074 public boolean isProperty() { 075 return property; 076 } 077 } 078 079 public static String getProperty(String value, PropertyResolver resolver) throws PropertyNotFoundException { 080 // optimization: we don't need another one 081 if (resolver instanceof LoopCheckerResolver) { 082 return resolver.resolveProperty(value, null); 083 } 084 return new LoopCheckerResolver(resolver).resolveProperty(value, resolver); 085 } 086 087 /** 088 * Filter <code>${name}</code> placeholders found within the value using given property resolver. 089 * @param value the value to filter the properties into 090 * @param propertyResolver resolver for the property values 091 * @return filtered value 092 * @throws PropertyNotFoundException when some property cannot be found 093 */ 094 public static String filter(String value, PropertyResolver propertyResolver) 095 throws PropertyNotFoundException { 096 List<Token> parts = splitIntoTokens(value); 097 StringBuilder result = new StringBuilder(); 098 099 for (Token part : parts) { 100 if (part.isProperty()) { 101 String propertyValue = getProperty(part.getValue(), propertyResolver); 102 if (propertyValue == null) { 103 throw new PropertyNotFoundException(part.getValue()); 104 //result.append(value.subSequence(position, endposition + 1)); 105 //break; 106 } 107 result.append(propertyValue); 108 } else { 109 result.append(part.getValue()); 110 } 111 } 112 return result.toString(); 113 } 114 115 /** 116 * Splits given string into {@link Token tokens}. 117 * <p> 118 * Token is ordinary text or property placeholder: <code>${name}</code>. 119 * For instance the string: "abc${abc}abc" will be split 120 * into three tokens: text "abc", property "abc" and text "abc". 121 * </p> 122 * @param input input string to split into tokens 123 * @return list of tokens 124 */ 125 public static List<Token> splitIntoTokens(String input) { 126 List<Token> parts = new ArrayList<Token>(); 127 int index = 0; 128 129 while (true) { 130 int startPosition = input.indexOf("${", index); 131 if (startPosition == -1) { 132 break; 133 } 134 // find '${' and '}' pair, correctly handle nested pairs 135 boolean lastDollar = false; 136 int count = 1; 137 int countBrace = 0; 138 int endposition; 139 for (endposition = startPosition + 2; endposition < input.length(); endposition++) { 140 if (input.charAt(endposition) == '$') { 141 lastDollar = true; 142 continue; 143 } 144 if (input.charAt(endposition) == '{') { 145 if (lastDollar) { 146 count++; 147 } else { 148 countBrace++; 149 } 150 } else if (input.charAt(endposition) == '}') { 151 if (countBrace > 0) { 152 countBrace--; 153 } else { 154 count--; 155 if (count == 0) { 156 break; 157 } 158 } 159 } 160 lastDollar = false; 161 } 162 163 if (count > 0) { 164 break; 165 } 166 167 if (index != startPosition) { 168 parts.add(new Token(input.substring(index, startPosition), false)); 169 } 170 171 String propertyName = input.substring(startPosition + 2, endposition); 172 index = endposition + 1; 173 174 parts.add(new Token(propertyName, true)); 175 } 176 177 if (index != input.length()) { 178 parts.add(new Token(input.substring(index), false)); 179 } 180 return parts; 181 } 182 183 /** 184 * Filter <code>${name}</code> placeholders found within the value using given properties. 185 * @param value the value to filter the properties into 186 * @param properties properties to filter into the value 187 * @return filtered value 188 * @throws PropertyNotFoundException when some property cannot be found 189 */ 190 public static String filter(String value, final Properties properties) 191 throws PropertyNotFoundException { 192 193 return filter(value, new PropertiesPropertyResolver(properties)); 194 } 195 196 /** 197 * Filter <code>${name}</code> placeholders found within the XML element. 198 * 199 * <p> 200 * The structure of the XML document is not changed. Each attribute and element text is filtered 201 * separately. 202 * </p> 203 * @param element 204 * @param propertyResolver 205 * @throws PropertyNotFoundException 206 */ 207 public static void filter(Element element, PropertyResolver propertyResolver) 208 throws PropertyNotFoundException { 209 XmlFiltering filtering = new XmlFiltering(propertyResolver); 210 filtering.filterElement(element); 211 } 212 213 /** 214 * Filter <code>${name}</code> placeholders found within the XML element. 215 * 216 * @see #filter(Element, PropertyResolver) 217 */ 218 public static void filter(Element element, Properties properties) 219 throws PropertyNotFoundException { 220 filter(element, new PropertiesPropertyResolver(properties)); 221 } 222 223 private static class XmlFiltering { 224 private PropertyResolver propertyResolver; 225 226 private XmlFiltering(PropertyResolver propertyResolver) { 227 this.propertyResolver = propertyResolver; 228 } 229 230 private void filterElement(Element element) throws PropertyNotFoundException { 231 NamedNodeMap attributes = element.getAttributes(); 232 for (int i = 0; i < attributes.getLength(); i++) { 233 Node node = attributes.item(i); 234 filterAttribute((Attr) node); 235 } 236 237 NodeList nodes = element.getChildNodes(); 238 for (int i = 0; i < nodes.getLength(); i++) { 239 Node node = nodes.item(i); 240 switch (node.getNodeType()) { 241 case Node.ELEMENT_NODE: 242 filterElement((Element) node); 243 break; 244 case Node.TEXT_NODE: 245 filterText((Text) node); 246 break; 247 } 248 } 249 } 250 251 private void filterAttribute(Attr attribute) throws PropertyNotFoundException { 252 attribute.setValue(filterString(attribute.getValue())); 253 } 254 255 private void filterText(Text text) throws PropertyNotFoundException { 256 text.setTextContent(filterString(text.getTextContent())); 257 } 258 259 private String filterString(String value) throws PropertyNotFoundException { 260 return Filtering.filter(value, propertyResolver); 261 } 262 } 263}