001    /*
002     * JBoss, Home of Professional Open Source.
003     * Copyright 2008, Red Hat Middleware LLC, and individual contributors
004     * as indicated by the @author tags. See the copyright.txt file in the
005     * distribution for a full listing of individual contributors. 
006     *
007     * This is free software; you can redistribute it and/or modify it
008     * under the terms of the GNU Lesser General Public License as
009     * published by the Free Software Foundation; either version 2.1 of
010     * the License, or (at your option) any later version.
011     *
012     * This software is distributed in the hope that it will be useful,
013     * but WITHOUT ANY WARRANTY; without even the implied warranty of
014     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015     * Lesser General Public License for more details.
016     *
017     * You should have received a copy of the GNU Lesser General Public
018     * License along with this software; if not, write to the Free
019     * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
020     * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
021     */
022    package org.jboss.dna.repository.sequencers;
023    
024    import java.io.Serializable;
025    import java.util.HashMap;
026    import java.util.Map;
027    import java.util.regex.Pattern;
028    import net.jcip.annotations.Immutable;
029    import org.jboss.dna.common.util.CheckArg;
030    import org.jboss.dna.common.util.HashCode;
031    import org.jboss.dna.graph.properties.PathExpression;
032    import org.jboss.dna.repository.RepositoryI18n;
033    
034    /**
035     * An expression that defines a selection of some change in the repository that signals a sequencing operation should be run, and
036     * the location where the sequencing output should be placed. Sequencer path expressions are used within the
037     * {@link SequencerConfig sequencer configurations} and used to determine whether information in the repository needs to be
038     * sequenced.
039     * <p>
040     * A simple example is the following:
041     * 
042     * <pre>
043     *     /a/b/c@title =&gt; /d/e/f
044     * </pre>
045     * 
046     * which means that a sequencer (that uses this expression in its configuration) should be run any time there is a new or modified
047     * <code>title</code> property on the <code>/a/b/c</code> node, and that the output of the sequencing should be placed at
048     * <code>/d/e/f</code>.
049     * </p>
050     * 
051     * @author Randall Hauch
052     */
053    @Immutable
054    public class SequencerPathExpression implements Serializable {
055    
056        /**
057         */
058        private static final long serialVersionUID = 229464314137494765L;
059    
060        /**
061         * The pattern used to break the initial input string into the two major parts, the selection and output expressions. Group 1
062         * contains the selection expression, and group 2 contains the output expression.
063         */
064        private static final Pattern TWO_PART_PATTERN = Pattern.compile("((?:[^=]|=(?!>))+)(?:=>(.+))?");
065    
066        protected static final String DEFAULT_OUTPUT_EXPRESSION = ".";
067    
068        private static final String PARENT_PATTERN_STRING = "[^/]+/\\.\\./"; // [^/]+/\.\./
069        private static final Pattern PARENT_PATTERN = Pattern.compile(PARENT_PATTERN_STRING);
070    
071        private static final String REPLACEMENT_VARIABLE_PATTERN_STRING = "(?<!\\\\)\\$(\\d+)"; // (?<!\\)\$(\d+)
072        private static final Pattern REPLACEMENT_VARIABLE_PATTERN = Pattern.compile(REPLACEMENT_VARIABLE_PATTERN_STRING);
073    
074        /**
075         * Compile the supplied expression and return the resulting SequencerPathExpression instance.
076         * 
077         * @param expression the expression
078         * @return the path expression; never null
079         * @throws IllegalArgumentException if the expression is null
080         * @throws InvalidSequencerPathExpression if the expression is blank or is not a valid expression
081         */
082        public static final SequencerPathExpression compile( String expression ) throws InvalidSequencerPathExpression {
083            CheckArg.isNotNull(expression, "sequencer path expression");
084            expression = expression.trim();
085            if (expression.length() == 0) {
086                throw new InvalidSequencerPathExpression(RepositoryI18n.pathExpressionMayNotBeBlank.text());
087            }
088            java.util.regex.Matcher matcher = TWO_PART_PATTERN.matcher(expression);
089            if (!matcher.matches()) {
090                throw new InvalidSequencerPathExpression(RepositoryI18n.pathExpressionIsInvalid.text(expression));
091            }
092            String selectExpression = matcher.group(1);
093            String outputExpression = matcher.group(2);
094            return new SequencerPathExpression(PathExpression.compile(selectExpression), outputExpression);
095        }
096    
097        private final PathExpression selectExpression;
098        private final String outputExpression;
099        private final int hc;
100    
101        protected SequencerPathExpression( PathExpression selectExpression,
102                                           String outputExpression ) throws InvalidSequencerPathExpression {
103            CheckArg.isNotNull(selectExpression, "select expression");
104            this.selectExpression = selectExpression;
105            this.outputExpression = outputExpression != null ? outputExpression.trim() : DEFAULT_OUTPUT_EXPRESSION;
106            this.hc = HashCode.compute(this.selectExpression, this.outputExpression);
107        }
108    
109        /**
110         * @return selectExpression
111         */
112        public String getSelectExpression() {
113            return this.selectExpression.getSelectExpression();
114        }
115    
116        /**
117         * @return outputExpression
118         */
119        public String getOutputExpression() {
120            return this.outputExpression;
121        }
122    
123        /**
124         * {@inheritDoc}
125         */
126        @Override
127        public int hashCode() {
128            return this.hc;
129        }
130    
131        /**
132         * {@inheritDoc}
133         */
134        @Override
135        public boolean equals( Object obj ) {
136            if (obj == this) return true;
137            if (obj instanceof SequencerPathExpression) {
138                SequencerPathExpression that = (SequencerPathExpression)obj;
139                if (!this.selectExpression.equals(that.selectExpression)) return false;
140                if (!this.outputExpression.equalsIgnoreCase(that.outputExpression)) return false;
141                return true;
142            }
143            return false;
144        }
145    
146        /**
147         * {@inheritDoc}
148         */
149        @Override
150        public String toString() {
151            return this.selectExpression + "=>" + this.outputExpression;
152        }
153    
154        /**
155         * @param absolutePath
156         * @return the matcher
157         */
158        public Matcher matcher( String absolutePath ) {
159            PathExpression.Matcher inputMatcher = selectExpression.matcher(absolutePath);
160            String outputPath = null;
161            if (inputMatcher.matches()) {
162                // Grab the named groups ...
163                Map<Integer, String> replacements = new HashMap<Integer, String>();
164                for (int i = 0, count = inputMatcher.groupCount(); i <= count; ++i) {
165                    replacements.put(i, inputMatcher.group(i));
166                }
167    
168                // Grab the selected path ...
169                String selectedPath = inputMatcher.getSelectedNodePath();
170    
171                // Find the output path using the groups from the match pattern ...
172                outputPath = this.outputExpression;
173                if (!DEFAULT_OUTPUT_EXPRESSION.equals(outputPath)) {
174                    java.util.regex.Matcher replacementMatcher = REPLACEMENT_VARIABLE_PATTERN.matcher(outputPath);
175                    StringBuffer sb = new StringBuffer();
176                    if (replacementMatcher.find()) {
177                        do {
178                            String variable = replacementMatcher.group(1);
179                            String replacement = replacements.get(Integer.valueOf(variable));
180                            if (replacement == null) replacement = replacementMatcher.group(0);
181                            replacementMatcher.appendReplacement(sb, replacement);
182                        } while (replacementMatcher.find());
183                        replacementMatcher.appendTail(sb);
184                        outputPath = sb.toString();
185                    }
186                    // Make sure there is a trailing '/' ...
187                    if (!outputPath.endsWith("/")) outputPath = outputPath + "/";
188    
189                    // Replace all references to "/./" with "/" ...
190                    outputPath = outputPath.replaceAll("/\\./", "/");
191    
192                    // Remove any path segment followed by a parent reference ...
193                    java.util.regex.Matcher parentMatcher = PARENT_PATTERN.matcher(outputPath);
194                    while (parentMatcher.find()) {
195                        outputPath = parentMatcher.replaceAll("");
196                        // Make sure there is a trailing '/' ...
197                        if (!outputPath.endsWith("/")) outputPath = outputPath + "/";
198                        parentMatcher = PARENT_PATTERN.matcher(outputPath);
199                    }
200    
201                    // Remove all multiple occurrences of '/' ...
202                    outputPath = outputPath.replaceAll("/{2,}", "/");
203    
204                    // Remove the trailing '/@property' ...
205                    outputPath = outputPath.replaceAll("/@[^/\\[\\]]+$", "");
206    
207                    // Remove a trailing '/' ...
208                    outputPath = outputPath.replaceAll("/$", "");
209    
210                    // If the output path is blank, then use the default output expression ...
211                    if (outputPath.length() == 0) outputPath = DEFAULT_OUTPUT_EXPRESSION;
212    
213                }
214                if (DEFAULT_OUTPUT_EXPRESSION.equals(outputPath)) {
215                    // The output path is the default expression, so use the selected path ...
216                    outputPath = selectedPath;
217                }
218            }
219    
220            return new Matcher(inputMatcher, outputPath);
221        }
222    
223        @Immutable
224        public static class Matcher {
225    
226            private final PathExpression.Matcher inputMatcher;
227            private final String outputPath;
228            private final int hc;
229    
230            protected Matcher( PathExpression.Matcher inputMatcher,
231                               String outputPath ) {
232                this.inputMatcher = inputMatcher;
233                this.outputPath = outputPath;
234                this.hc = HashCode.compute(super.hashCode(), this.outputPath);
235            }
236    
237            public boolean matches() {
238                return inputMatcher.matches() && this.outputPath != null;
239            }
240    
241            /**
242             * @return inputPath
243             */
244            public String getInputPath() {
245                return inputMatcher.getInputPath();
246            }
247    
248            /**
249             * @return selectPattern
250             */
251            public String getSelectedPath() {
252                return inputMatcher.getSelectedNodePath();
253            }
254    
255            /**
256             * @return outputPath
257             */
258            public String getOutputPath() {
259                return this.outputPath;
260            }
261    
262            /**
263             * {@inheritDoc}
264             */
265            @Override
266            public int hashCode() {
267                return this.hc;
268            }
269    
270            /**
271             * {@inheritDoc}
272             */
273            @Override
274            public boolean equals( Object obj ) {
275                if (obj == this) return true;
276                if (obj instanceof SequencerPathExpression.Matcher) {
277                    SequencerPathExpression.Matcher that = (SequencerPathExpression.Matcher)obj;
278                    if (!super.equals(that)) return false;
279                    if (!this.outputPath.equalsIgnoreCase(that.outputPath)) return false;
280                    return true;
281                }
282                return false;
283            }
284    
285            /**
286             * {@inheritDoc}
287             */
288            @Override
289            public String toString() {
290                return inputMatcher + " => " + this.outputPath;
291            }
292        }
293    
294    }