Source code: org/scopemvc/core/Selector.java
1 /*
2 * Scope: a generic MVC framework.
3 * Copyright (c) 2000-2002, Steve Meyfroidt
4 * All rights reserved.
5 * Email: smeyfroi@users.sourceforge.net
6 *
7 *
8 * Redistribution and use in source and binary forms, with or without
9 * modification, are permitted provided that the following conditions
10 * are met:
11 *
12 * Redistributions of source code must retain the above copyright
13 * notice, this list of conditions and the following disclaimer.
14 *
15 * Redistributions in binary form must reproduce the above copyright
16 * notice, this list of conditions and the following disclaimer in the
17 * documentation and/or other materials provided with the distribution.
18 *
19 * Neither the name "Scope" nor the names of its contributors
20 * may be used to endorse or promote products derived from this software
21 * without specific prior written permission.
22 *
23 *
24 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
25 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
26 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
27 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
28 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
29 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
30 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
31 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
32 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
33 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
34 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35 *
36 *
37 * $Id: Selector.java,v 1.4 2002/01/26 09:46:20 smeyfroi Exp $
38 */
39
40
41 package org.scopemvc.core;
42
43
44 import java.util.StringTokenizer;
45 import org.apache.commons.logging.Log;
46 import org.apache.commons.logging.LogSource;
47 import org.scopemvc.util.Debug;
48
49
50 /**
51 * <P>
52 * An identifier for model properties. Selectors are
53 * created by the factory methods {@link #fromString}
54 * and {@link #fromInt}. Properties can be identified
55 * by a String name (eg the "address" property of
56 * a Customer) or an integer index (eg element 1 of
57 * a List).
58 * </P>
59 * <P>
60 * Selectors can be assembled in a list to identify a property in a model
61 * contained within another model. For example, the <CODE>name</CODE>
62 * of the <CODE>pet</CODE> of a <CODE>Person</CODE>. This
63 * Selector would be created by <CODE>Selector.fromString("pet.name")</CODE>
64 * and applied to a Person model object. Similarly, the name of a
65 * Person's first pet could be identified using
66 * <CODE>Selector.fromString("pets.0.name")</CODE> assuming
67 * that Person contains a List or array of Pets that contain a name property.
68 * </P>
69 *
70 * @author <A HREF="mailto:smeyfroi@users.sourceforge.net">Steve Meyfroidt</A>
71 * @version $Revision: 1.4 $ $Date: 2002/01/26 09:46:20 $
72 */
73 public abstract class Selector {
74
75
76 private static final Log LOG = LogSource.getInstance(Selector.class);
77
78
79 /**
80 * Link to next Selector in the list.
81 */
82 private Selector next;
83
84
85 /**
86 * Separator character between Selectors
87 * expressed as a String.
88 *
89 * @see #fromString
90 * @see #asString
91 */
92 public static String DELIMITER = ".";
93
94
95 /**
96 * Package private ctor for the factory. Application
97 * code should use {@link Selector#fromString}
98 * or {@link Selector#fromInt} to create Selectors.
99 */
100 Selector() {
101 }
102
103
104 /**
105 * Get the next Selector in the list, if any. For example,
106 * <PRE>
107 * Selector petNameSelector = Selector.fromString("pet.name");
108 * return petNameSelector.getNext();
109 * </PRE>
110 * returns a Selector that is <CODE>equals()</CODE> the following Selector:
111 * <PRE>
112 * Selector.fromString("name");
113 * </PRE>
114 */
115 public final Selector getNext() {
116 return next;
117 }
118
119
120 /**
121 * Set the next Selector in the list.
122 *
123 * @param inSelector Selector to set as the next in the list after <CODE>this</CODE>.
124 * @see #chain
125 */
126 protected final void setNext(Selector inSelector) {
127 next = inSelector;
128 }
129
130
131 /**
132 * <P>
133 * Add a Selector on the end of this list.
134 * </P>
135 * <P>
136 * For example:
137 * <PRE>
138 * Selector petSelector = Selector.fromString("pet");
139 * Selector nameSelector = Selector.fromString("name");
140 * petSelector.chain(nameSelector);
141 * return petSelector;
142 * </PRE>
143 * returns a Selector that is <CODE>equals()</CODE> this one:
144 * <PRE>
145 * Selector.fromString("pet.name");
146 * </PRE>
147 * </P>
148 */
149 public final void chain(Selector inSelector) {
150 if (inSelector == null) {
151 throw new IllegalArgumentException("Can't chain a null Selector.");
152 }
153
154 ((Selector)getLast()).setNext(inSelector);
155 }
156
157
158 /**
159 * Get the last Selector in the list. For example,
160 * <PRE>
161 * Selector petNameSelector = Selector.fromString("pets.1.name");
162 * return petNameSelector.getLast();
163 * </PRE>
164 * returns a Selector that is <CODE>equals()</CODE> the following Selector:
165 * <PRE>
166 * Selector.fromString("name");
167 * </PRE>
168 */
169 public final Selector getLast() {
170 Selector result = this;
171 while (result.getNext() != null) {
172 result = result.getNext();
173 }
174
175 if (Debug.ON) Debug.assertTrue(result != null);
176 return result;
177 }
178
179
180 /**
181 * Remove the terminal Selector. Throws UnsupportedOperationException
182 * if Selector has no chain.
183 */
184 public final void removeLast() {
185 if (getNext() == null) {
186 throw new UnsupportedOperationException("No terminal Selector to remove");
187 }
188 Selector penultimate = this;
189 while (penultimate.getNext().getNext() != null) {
190 penultimate = penultimate.getNext();
191 }
192 penultimate.setNext(null);
193 }
194
195
196 /**
197 * <P>
198 * Does this Selector list start with the list passed in?
199 * </P>
200 * <P>
201 * For example, this returns <CODE>true</CODE>:
202 * <PRE>
203 * Selector petSelector = Selector.fromString("pet");
204 * Selector petNameSelector = Selector.fromString("pet.name");
205 * return (petNameSelector.startsWith(petSelector));
206 * </PRE>
207 * but this returns <CODE>false</CODE>:
208 * <PRE>
209 * Selector nameSelector = Selector.fromString("name");
210 * Selector petNameSelector = Selector.fromString("pet.name");
211 * return (petNameSelector.startsWith(nameSelector));
212 * </PRE>
213 * </P>
214 */
215 public final boolean startsWith(Selector inSelector) {
216 Selector matchedTarget = inSelector;
217 Selector matchedThis = this;
218 while (matchedTarget != null) {
219 if (matchedThis == null) {
220 // passed in is longer than this
221 return false;
222 }
223 if (matchedThis.shallowEquals(matchedTarget)) {
224 matchedThis = matchedThis.getNext();
225 matchedTarget = matchedTarget.getNext();
226 } else {
227 // No match
228 return false;
229 }
230 }
231 return true;
232 }
233
234
235 /**
236 * Compare the head Selector of <CODE>this</CODE>
237 * against the head of another Selector list --
238 * ie a shallow compare operation (not including the
239 * chained selectors).
240 */
241 protected abstract boolean shallowEquals(Selector inSelector);
242
243
244 /**
245 * <P>
246 * A deep compare, following down the list of selectors.
247 * </P>
248 * <P>
249 * For example, this returns <CODE>true</CODE>:
250 * <PRE>
251 * Selector petSelector = Selector.fromString("pet");
252 * Selector nameSelector = Selector.fromString("name");
253 * Selector petNameSelector = Selector.fromString("pet.name");
254 * petSelector.chain(nameSelector);
255 * return (petSelector.equals(petNameSelector));
256 * </PRE>
257 * but this returns <CODE>false</CODE>:
258 * <PRE>
259 * Selector petSelector = Selector.fromString("pet");
260 * Selector petNameSelector = Selector.fromString("pet.name");
261 * return (petSelector.equals(petNameSelector));
262 * </PRE>
263 * </P>
264 */
265 public final boolean equals(Object inObject) {
266
267 // Trivial case: same object
268 if (inObject == this) {
269 return true;
270 }
271
272 // null or wrong class
273 if (! (inObject instanceof Selector)) {
274 return false;
275 }
276
277 // try the shallow equals
278 Selector inSelector = (Selector)inObject;
279 if (! shallowEquals(inSelector)) {
280 return false;
281 }
282
283 if (getNext() == null) {
284 if (inSelector.getNext() == null) {
285 return true;
286 }
287 return false;
288 }
289
290 // and recurse down the list
291 return getNext().equals(inSelector.getNext());
292 }
293
294
295 /**
296 * Return a clone of the entire list of
297 * Selectors from <CODE>this</CODE>.
298 */
299 public final Selector deepClone() {
300 Selector result = getShallowCopy();
301 if (getNext() != null) {
302 result.chain(getNext().deepClone());
303 }
304 return result;
305 }
306
307
308 /**
309 * Return a shallow copy of the head of <CODE>this</CODE>.
310 */
311 protected abstract Selector getShallowCopy();
312
313
314 /**
315 * Used to serialise Selectors {@link #asString}
316 * and for debug by {@link #toString}.
317 * ***** public access for test cases -- this is nasty.
318 */
319 public abstract String getName();
320
321
322 // ------------------- Debug -----------------------
323
324
325 /**
326 * For debug.
327 */
328 public final String toString() {
329 StringBuffer result = new StringBuffer();
330 Package p = getClass().getPackage();
331 if (p == null) {
332 result.append(getClass().getName());
333 } else {
334 result.append(getClass().getName().substring(p.getName().length() + 1));
335 }
336 result.append("(");
337 result.append(Selector.asString(this));
338 result.append(")");
339 return result.toString();
340 }
341
342
343 // -------------------- Factory -------------------------
344
345
346 /**
347 * Make a simple Selector to identify a property at
348 * an int index. eg this returns a Selector that identifies
349 * the first element of a List:
350 * <PRE>
351 * return Selector.fromInt(0);
352 * </PRE>
353 */
354 public static IntIndexSelector fromInt(int inIndex) {
355 return new IntIndexSelector(inIndex);
356 }
357
358
359 /**
360 * Make a Selector to identify a property by its String name,
361 * or to create a Selector list that identifies a property by
362 * navigating a hierarchy of submodels. For example:
363 * <UL>
364 * <LI>
365 * <CODE>Selector.fromString("name");</CODE>
366 * will identify the name property of a Person
367 * when applied to a Person model object.
368 * </LI>
369 * <CODE>Selector.fromString("pet.0.name");</CODE>
370 * will identify the name property of the first Pet of a Person
371 * when applied to a Person model object.
372 * </UL>
373 */
374 public static Selector fromString(String inSelectorDescription) {
375 if (LOG.isDebugEnabled()) LOG.debug("fromString: " + inSelectorDescription);
376
377 StringTokenizer tokenizer = new StringTokenizer(inSelectorDescription, DELIMITER);
378 Selector result = null;
379
380 while (tokenizer.hasMoreTokens()) {
381 String nextToken = tokenizer.nextToken();
382 if (LOG.isDebugEnabled()) LOG.debug("fromString: " + nextToken);
383
384 // Empty property descriptor is a null Selector
385 if (nextToken.length() < 1) {
386 continue;
387 }
388
389 Selector nextSelector = null;
390 // OK then, does it parse as an int so we get an IntIndexSelector?
391 // ***** this is a bit nasty. could use [0] notation to mark this?
392 try {
393 int intIndex = Integer.parseInt(nextToken);
394 nextSelector = new IntIndexSelector(intIndex);
395 } catch (NumberFormatException e) {
396 // OK then so make a StringIndexSelector
397 nextSelector = new StringIndexSelector(nextToken);
398 }
399
400 if (LOG.isDebugEnabled()) LOG.debug("fromString: " + nextToken + ": " + nextSelector);
401 if (Debug.ON) Debug.assertTrue(nextSelector != null, "Couldn't create Selector for: " + nextToken);
402
403 if (result == null) {
404 result = nextSelector;
405 } else {
406 result.chain(nextSelector);
407 }
408 if (LOG.isDebugEnabled()) LOG.debug("fromString: result: " + result);
409 }
410 return result;
411 }
412
413
414 /**
415 * Flatten the Selector list to a String that could
416 * be passed into {@link #fromString} to recreate it.
417 *
418 * @param inSelector flatten this Selector to a String representation.
419 * @return String representation of the passed Selector suitable
420 * for passing back into {@link #fromString} to
421 * recreate the Selector list. Return "" for null Selector.
422 */
423 public static String asString(Selector inSelector) {
424 if (inSelector == null) {
425 return "";
426 }
427
428 StringBuffer result = new StringBuffer();
429 Selector current = inSelector;
430 while (current != null) {
431 result.append(current.getName());
432 result.append(DELIMITER);
433 current = current.getNext();
434 }
435 return result.substring(0, result.length() - DELIMITER.length());
436 }
437 }
438