001    /**   
002     * Copyright 2011 The Buzz Media, LLC
003     * 
004     * Licensed under the Apache License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     *   http://www.apache.org/licenses/LICENSE-2.0
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package com.thebuzzmedia.sjxp.rule;
017    
018    import com.thebuzzmedia.sjxp.XMLParser;
019    
020    /**
021     * Class used to provide a default implementation of a rule in SJXP.
022     * <p/>
023     * It is intended that you only ever need to use this class and not implement
024     * your own {@link IRule} classes yourself (you are certainly welcome to
025     * though).
026     * <p/>
027     * Rules all consist of the same boiler plate:
028     * <ul>
029     * <li>A {@link IRule.Type}, so we know if the rule wants to match character
030     * data or attribute values.</li>
031     * <li>A <code>locationPath</code>, which tells us the path to the element in
032     * the XML document to match.</li>
033     * <li>OPTIONAL: 1 or more <code>attributeNames</code> from the matching element
034     * that we want values from.</li>
035     * </ul>
036     * All of that rudimentary behavior, along with some nice error-checking and an
037     * easy-to-debug and caching <code>toString</code> implementation are provided
038     * by this class so you can hit the ground running by simply creating an
039     * instance of this class, passing it a location path and provided an
040     * implementation for the handler you are interested in.
041     * <p/>
042     * An example would look like this:
043     * 
044     * <pre>
045     * new DefaultRule(Type.CHARACTER, &quot;/library/book/title&quot;) {
046     *      &#064;Override
047     *      public void handleParsedCharacters(XMLParser parser, String text, T userObject) {
048     *              // Handle the title text
049     *      }
050     * };
051     * </pre>
052     * 
053     * <h3>Instance Reuse</h3>
054     * Instances of {@link DefaultRule} are immutable and maintain no internal
055     * state, so re-using the same {@link DefaultRule} among multiple instances of
056     * {@link XMLParser} is safe.
057     * 
058     * @param <T>
059     *            The class type of any user-supplied object that the caller wishes
060     *            to be passed through from one of the {@link XMLParser}'s
061     *            <code>parse</code> methods directly to the handler when a rule
062     *            matches. This is typically a data storage mechanism like a DAO or
063     *            cache used to store the parsed value in some valuable way, but it
064     *            can ultimately be anything. If you do not need to make use of the
065     *            user object, there is no need to parameterize the class.
066     * 
067     * @author Riyad Kalla (software@thebuzzmedia.com)
068     */
069    public class DefaultRule<T> implements IRule<T> {
070            private String toStringCache = null;
071    
072            private Type type;
073            private String locationPath;
074            private String[] attributeNames;
075    
076            /**
077             * Create a new rule with the given values.
078             * 
079             * @param type
080             *            The type of the rule.
081             * @param locationPath
082             *            The location path of the element to target in the XML.
083             * @param attributeNames
084             *            An optional list of attribute names to parse values for if the
085             *            type of this rule is {@link IRule.Type#ATTRIBUTE}.
086             * 
087             * @throws IllegalArgumentException
088             *             if <code>type</code> is <code>null</code>, if
089             *             <code>locationPath</code> is <code>null</code> or empty, if
090             *             <code>type</code> is {@link IRule.Type#ATTRIBUTE} and
091             *             <code>attributeNames</code> is <code>null</code> or empty or
092             *             if <code>type</code> is {@link IRule.Type#CHARACTER} and
093             *             <code>attributeNames</code> <strong>is not</strong>
094             *             <code>null</code> or empty.
095             */
096            public DefaultRule(Type type, String locationPath, String... attributeNames)
097                            throws IllegalArgumentException {
098                    if (type == null)
099                            throw new IllegalArgumentException("type cannot be null");
100                    if (locationPath == null || locationPath.length() == 0)
101                            throw new IllegalArgumentException(
102                                            "locationPath cannot be null or empty");
103                    /*
104                     * Pedantic, while we could remove a single trailing slash easily
105                     * enough, there is the very-small-chance the users has multiple
106                     * trailing slashes... again easy to remove, but at this point they are
107                     * being really sloppy and we are letting it slide. Instead, fire an
108                     * exception up-front and teach people how the API behaves immediately
109                     * and what is required. Makes everyone's lives easier.
110                     */
111                    if (locationPath.charAt(locationPath.length() - 1) == '/')
112                            throw new IllegalArgumentException(
113                                            "locationPath cannot end in a trailing slash (/), please remove it.");
114                    if ((type == Type.ATTRIBUTE && (attributeNames == null || attributeNames.length == 0)))
115                            throw new IllegalArgumentException(
116                                            "Type.ATTRIBUTE was specified but attributeNames was null or empty. One or more attribute names must be provided for this rule if it is going to match any attribute values.");
117                    /*
118                     * Pedantic, but it will warn the caller of what is likely an
119                     * programming error condition very early on so they don't bang their
120                     * head against the wall as to why the parser isn't picking up their
121                     * attributes.
122                     */
123                    if (type == Type.CHARACTER && attributeNames != null
124                                    && attributeNames.length > 0)
125                            throw new IllegalArgumentException(
126                                            "Type.CHARACTER was specified, but attribute names were passed in. This is likely a mistake and can be fixed by simply not passing in the ignored attribute names.");
127    
128                    this.type = type;
129                    this.locationPath = locationPath;
130                    this.attributeNames = attributeNames;
131            }
132    
133            /**
134             * Overridden to provide a nicely formatted representation of the rule for
135             * easy debugging.
136             * <p/>
137             * As an added bonus, since {@link IRule}s are intended to be immutable, the
138             * result of <code>toString</code> is cached on the first call and the cache
139             * returned every time to avoid re-computing the completed {@link String}.
140             * 
141             * @return a nicely formatted representation of the rule for easy debugging.
142             */
143            @Override
144            public synchronized String toString() {
145                    if (toStringCache == null) {
146                            StringBuilder builder = null;
147    
148                            /*
149                             * toString is only used during debugging, so make the toString
150                             * output of the rule pretty so it is easier to track in debug
151                             * messages.
152                             */
153                            if (attributeNames != null && attributeNames.length > 0) {
154                                    builder = new StringBuilder();
155    
156                                    for (String name : attributeNames)
157                                            builder.append(name).append(',');
158    
159                                    // Chop the last stray comma
160                                    builder.setLength(builder.length() - 1);
161                            }
162    
163                            toStringCache = this.getClass().getName() + "[type=" + type
164                                            + ", locationPath=" + locationPath + ", attributeNames="
165                                            + (builder == null ? "" : builder.toString()) + "]";
166                    }
167    
168                    return toStringCache;
169            }
170    
171            public Type getType() {
172                    return type;
173            }
174    
175            public String getLocationPath() {
176                    return locationPath;
177            }
178    
179            public String[] getAttributeNames() {
180                    return attributeNames;
181            }
182    
183            /**
184             * Default no-op implementation. Please override with your own logic.
185             * 
186             * @see IRule#handleTag(XMLParser, boolean, Object)
187             */
188            public void handleTag(XMLParser<T> parser, boolean isStartTag, T userObject) {
189                    // no-op impl
190            }
191    
192            /**
193             * Default no-op implementation. Please override with your own logic.
194             * 
195             * @see IRule#handleParsedAttribute(XMLParser, int, String, Object)
196             */
197            public void handleParsedAttribute(XMLParser<T> parser, int index,
198                            String value, T userObject) {
199                    // no-op impl
200            }
201    
202            /**
203             * Default no-op implementation. Please override with your own logic.
204             * 
205             * @see IRule#handleParsedCharacters(XMLParser, String, Object)
206             */
207            public void handleParsedCharacters(XMLParser<T> parser, String text,
208                            T userObject) {
209                    // no-op impl
210            }
211    }