View Javadoc

1   /*
2    * ModeShape (http://www.modeshape.org)
3    * See the COPYRIGHT.txt file distributed with this work for information
4    * regarding copyright ownership.  Some portions may be licensed
5    * to Red Hat, Inc. under one or more contributor license agreements.
6    * See the AUTHORS.txt file in the distribution for a full listing of 
7    * individual contributors.
8    *
9    * ModeShape is free software. Unless otherwise indicated, all code in ModeShape
10   * is licensed to you under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation; either version 2.1 of
12   * the License, or (at your option) any later version.
13   * 
14   * ModeShape is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17   * Lesser General Public License for more details.
18   *
19   * You should have received a copy of the GNU Lesser General Public
20   * License along with this software; if not, write to the Free
21   * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
22   * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
23   */
24  package org.modeshape.graph.query.model;
25  
26  import java.text.CharacterIterator;
27  import java.text.StringCharacterIterator;
28  import java.util.Iterator;
29  import java.util.List;
30  import net.jcip.annotations.Immutable;
31  import org.modeshape.common.text.ParsingException;
32  import org.modeshape.common.util.CheckArg;
33  import org.modeshape.common.util.HashCode;
34  import org.modeshape.common.util.ObjectUtil;
35  import org.modeshape.graph.query.parse.FullTextSearchParser;
36  
37  /**
38   * A constraint that evaluates to true only when a full-text search applied to the search scope results in positive findings. If a
39   * property name is supplied, then the search is limited to the value(s) of the named property on the node(s) in the search scope.
40   */
41  @Immutable
42  public class FullTextSearch implements Constraint {
43      private static final long serialVersionUID = 1L;
44  
45      private final SelectorName selectorName;
46      private final String propertyName;
47      private final String fullTextSearchExpression;
48      private Term term;
49      private final int hc;
50  
51      /**
52       * Create a constraint defining a full-text search against the property values on node within the search scope.
53       * 
54       * @param selectorName the name of the node selector defining the search scope
55       * @param propertyName the name of the property to be searched; may be null if all property values are to be searched
56       * @param fullTextSearchExpression the search expression
57       * @param term the term representation, if it is known; may be null
58       */
59      public FullTextSearch( SelectorName selectorName,
60                             String propertyName,
61                             String fullTextSearchExpression,
62                             Term term ) {
63          CheckArg.isNotNull(selectorName, "selectorName");
64          CheckArg.isNotEmpty(fullTextSearchExpression, "fullTextSearchExpression");
65          this.selectorName = selectorName;
66          this.propertyName = propertyName;
67          this.term = term;
68          this.fullTextSearchExpression = fullTextSearchExpression;
69          this.hc = HashCode.compute(this.selectorName, this.propertyName, this.fullTextSearchExpression);
70      }
71  
72      /**
73       * Create a constraint defining a full-text search against the property values on node within the search scope.
74       * 
75       * @param selectorName the name of the node selector defining the search scope
76       * @param propertyName the name of the property to be searched; may be null if all property values are to be searched
77       * @param fullTextSearchExpression the search expression
78       */
79      public FullTextSearch( SelectorName selectorName,
80                             String propertyName,
81                             String fullTextSearchExpression ) {
82          CheckArg.isNotNull(selectorName, "selectorName");
83          CheckArg.isNotEmpty(fullTextSearchExpression, "fullTextSearchExpression");
84          this.selectorName = selectorName;
85          this.propertyName = propertyName;
86          this.fullTextSearchExpression = fullTextSearchExpression;
87          this.term = null;
88          this.hc = HashCode.compute(this.selectorName, this.propertyName, this.fullTextSearchExpression);
89      }
90  
91      /**
92       * Create a constraint defining a full-text search against the node within the search scope.
93       * 
94       * @param selectorName the name of the node selector defining the search scope
95       * @param fullTextSearchExpression the search expression
96       */
97      public FullTextSearch( SelectorName selectorName,
98                             String fullTextSearchExpression ) {
99          CheckArg.isNotNull(selectorName, "selectorName");
100         CheckArg.isNotEmpty(fullTextSearchExpression, "fullTextSearchExpression");
101         this.selectorName = selectorName;
102         this.propertyName = null;
103         this.term = null;
104         this.fullTextSearchExpression = fullTextSearchExpression;
105         this.hc = HashCode.compute(this.selectorName, this.propertyName, this.fullTextSearchExpression);
106     }
107 
108     /**
109      * Get the name of the selector that is to be searched
110      * 
111      * @return the selector name; never null
112      */
113     public final SelectorName selectorName() {
114         return selectorName;
115     }
116 
117     /**
118      * Get the name of the property that is to be searched.
119      * 
120      * @return the property name; or null if the full-text search is to be performed across all searchable properties
121      */
122     public final String propertyName() {
123         return propertyName;
124     }
125 
126     /**
127      * Get the full-text search expression, as a string.
128      * 
129      * @return the search expression; never null
130      */
131     public final String fullTextSearchExpression() {
132         return fullTextSearchExpression;
133     }
134 
135     /**
136      * Get the formal {@link Term} representation of the expression.
137      * 
138      * @return the term representing this search; never null
139      * @throws ParsingException if there is an error producing the term representation
140      */
141     public Term getTerm() {
142         // Idempotent, so okay to not lock/synchronize ...
143         if (term == null) {
144             term = new FullTextSearchParser().parse(fullTextSearchExpression);
145         }
146         return term;
147     }
148 
149     /**
150      * {@inheritDoc}
151      * 
152      * @see java.lang.Object#toString()
153      */
154     @Override
155     public String toString() {
156         return Visitors.readable(this);
157     }
158 
159     /**
160      * {@inheritDoc}
161      * 
162      * @see java.lang.Object#hashCode()
163      */
164     @Override
165     public int hashCode() {
166         return hc;
167     }
168 
169     /**
170      * {@inheritDoc}
171      * 
172      * @see java.lang.Object#equals(java.lang.Object)
173      */
174     @Override
175     public boolean equals( Object obj ) {
176         if (obj == this) return true;
177         if (obj instanceof FullTextSearch) {
178             FullTextSearch that = (FullTextSearch)obj;
179             if (this.hc != that.hc) return false;
180             if (!this.selectorName.equals(that.selectorName)) return false;
181             if (!ObjectUtil.isEqualWithNulls(this.propertyName, that.propertyName)) return false;
182             if (!this.fullTextSearchExpression.equals(that.fullTextSearchExpression)) return false;
183             return true;
184         }
185         return false;
186     }
187 
188     /**
189      * {@inheritDoc}
190      * 
191      * @see org.modeshape.graph.query.model.Visitable#accept(org.modeshape.graph.query.model.Visitor)
192      */
193     public void accept( Visitor visitor ) {
194         visitor.visit(this);
195     }
196 
197     /**
198      * The general notion of a term that makes up a full-text search.
199      */
200     public static interface Term {
201     }
202 
203     /**
204      * A {@link Term} that represents a search term that requires another term to not appear.
205      */
206     public static class NegationTerm implements Term {
207         private final Term negated;
208 
209         public NegationTerm( Term negatedTerm ) {
210             assert negatedTerm != null;
211             this.negated = negatedTerm;
212         }
213 
214         /**
215          * Get the term that is negated.
216          * 
217          * @return the negated term; never null
218          */
219         public Term getNegatedTerm() {
220             return negated;
221         }
222 
223         /**
224          * {@inheritDoc}
225          * 
226          * @see java.lang.Object#hashCode()
227          */
228         @Override
229         public int hashCode() {
230             return negated.hashCode();
231         }
232 
233         /**
234          * {@inheritDoc}
235          * 
236          * @see java.lang.Object#equals(java.lang.Object)
237          */
238         @Override
239         public boolean equals( Object obj ) {
240             if (obj == this) return true;
241             if (obj instanceof NegationTerm) {
242                 NegationTerm that = (NegationTerm)obj;
243                 return this.getNegatedTerm().equals(that.getNegatedTerm());
244             }
245             return false;
246         }
247 
248         /**
249          * {@inheritDoc}
250          * 
251          * @see java.lang.Object#toString()
252          */
253         @Override
254         public String toString() {
255             return "-" + negated.toString();
256         }
257     }
258 
259     /**
260      * A {@link Term} that represents a single search term. The term may be comprised of multiple words.
261      */
262     public static class SimpleTerm implements Term {
263         private final String value;
264         private final boolean quoted;
265 
266         /**
267          * Create a simple term with the value and whether the term is excluded or included.
268          * 
269          * @param value the value that makes up the term
270          */
271         public SimpleTerm( String value ) {
272             assert value != null;
273             assert value.trim().length() > 0;
274             this.value = value;
275             this.quoted = this.value.indexOf(' ') != -1;
276         }
277 
278         /**
279          * Get the value of this term. Note that this is the actual value that is to be searched for, and will not include the
280          * {@link #isQuotingRequired() quotes}.
281          * 
282          * @return the value; never null
283          */
284         public String getValue() {
285             return value;
286         }
287 
288         /**
289          * Get the values of this term if the term is quoted.
290          * 
291          * @return the array of terms; never null
292          */
293         public String[] getValues() {
294             return value.split("/w");
295         }
296 
297         /**
298          * Get whether this term needs to be quoted because it consists of multiple words.
299          * 
300          * @return true if the term needs to be quoted, or false otherwise
301          */
302         public boolean isQuotingRequired() {
303             return quoted;
304         }
305 
306         /**
307          * Return whether this term contains any unescaped wildcard characters (e.g., one of '*', '?', '%', or '_').
308          * 
309          * @return true if this term contains unescaped wildcard characters, or false otherwise
310          */
311         public boolean containsWildcards() {
312             if (this.value.length() == 0) return false;
313             CharacterIterator iter = new StringCharacterIterator(this.value);
314             boolean skipNext = false;
315             for (char c = iter.first(); c != CharacterIterator.DONE; c = iter.next()) {
316                 if (skipNext) {
317                     skipNext = false;
318                     continue;
319                 }
320                 if (c == '*' || c == '?' || c == '%' || c == '_') return true;
321                 if (c == '\\') skipNext = true;
322             }
323             return false;
324         }
325 
326         /**
327          * {@inheritDoc}
328          * 
329          * @see java.lang.Object#hashCode()
330          */
331         @Override
332         public int hashCode() {
333             return value.hashCode();
334         }
335 
336         /**
337          * {@inheritDoc}
338          * 
339          * @see java.lang.Object#equals(java.lang.Object)
340          */
341         @Override
342         public boolean equals( Object obj ) {
343             if (obj == this) return true;
344             if (obj instanceof SimpleTerm) {
345                 SimpleTerm that = (SimpleTerm)obj;
346                 return this.getValue().equals(that.getValue());
347             }
348             return false;
349         }
350 
351         /**
352          * {@inheritDoc}
353          * 
354          * @see java.lang.Object#toString()
355          */
356         @Override
357         public String toString() {
358             return quoted ? "\"" + this.value + "\"" : this.value;
359         }
360     }
361 
362     /**
363      * A list of {@link Term}s.
364      */
365     public static abstract class CompoundTerm implements Term, Iterable<Term> {
366         private final List<Term> terms;
367 
368         /**
369          * Create a compound term of the supplied terms.
370          * 
371          * @param terms the terms; may not be null or empty
372          */
373         protected CompoundTerm( List<Term> terms ) {
374             this.terms = terms;
375         }
376 
377         /**
378          * Get the terms that make up this compound term.
379          * 
380          * @return the terms in the disjunction; never null and never empty
381          */
382         public List<Term> getTerms() {
383             return terms;
384         }
385 
386         /**
387          * {@inheritDoc}
388          * 
389          * @see java.lang.Iterable#iterator()
390          */
391         public Iterator<Term> iterator() {
392             return terms.iterator();
393         }
394 
395         /**
396          * {@inheritDoc}
397          * 
398          * @see java.lang.Object#hashCode()
399          */
400         @Override
401         public int hashCode() {
402             return terms.hashCode();
403         }
404 
405         /**
406          * {@inheritDoc}
407          * 
408          * @see java.lang.Object#equals(java.lang.Object)
409          */
410         @Override
411         public boolean equals( Object obj ) {
412             if (obj == this) return true;
413             if (this.getClass().isInstance(obj)) {
414                 CompoundTerm that = (CompoundTerm)obj;
415                 return this.getTerms().equals(that.getTerms());
416             }
417             return false;
418         }
419 
420         protected String toString( String delimiter ) {
421             if (terms.size() == 1) return terms.iterator().next().toString();
422             StringBuilder sb = new StringBuilder();
423             sb.append("( ");
424             boolean first = true;
425             for (Term term : terms) {
426                 if (first) first = false;
427                 else sb.append(' ').append(delimiter).append(' ');
428                 sb.append(term);
429             }
430             sb.append(" )");
431             return sb.toString();
432         }
433     }
434 
435     /**
436      * A set of {@link Term}s that are ORed together.
437      */
438     public static class Disjunction extends CompoundTerm {
439 
440         /**
441          * Create a disjunction of the supplied terms.
442          * 
443          * @param terms the terms to be ORed together; may not be null or empty
444          */
445         public Disjunction( List<Term> terms ) {
446             super(terms);
447         }
448 
449         /**
450          * {@inheritDoc}
451          * 
452          * @see java.lang.Object#toString()
453          */
454         @Override
455         public String toString() {
456             return toString("OR");
457         }
458     }
459 
460     /**
461      * A set of {@link Term}s that are ANDed together.
462      */
463     public static class Conjunction extends CompoundTerm {
464 
465         /**
466          * Create a conjunction of the supplied terms.
467          * 
468          * @param terms the terms to be ANDed together; may not be null or empty
469          */
470         public Conjunction( List<Term> terms ) {
471             super(terms);
472         }
473 
474         /**
475          * {@inheritDoc}
476          * 
477          * @see java.lang.Object#toString()
478          */
479         @Override
480         public String toString() {
481             return toString("AND");
482         }
483     }
484 
485 }