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}