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