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, "/library/book/title") { 046 * @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 }