001
002 /*
003 * Copyright (C) 2011 Archie L. Cobbs. All rights reserved.
004 *
005 * $Id: SimpleObjectParser.java 98 2011-05-10 14:21:20Z archie.cobbs $
006 */
007
008 package org.dellroad.stuff.string;
009
010 import java.beans.BeanInfo;
011 import java.beans.IndexedPropertyDescriptor;
012 import java.beans.IntrospectionException;
013 import java.beans.Introspector;
014 import java.beans.PropertyDescriptor;
015 import java.lang.reflect.Method;
016 import java.util.Collections;
017 import java.util.HashMap;
018 import java.util.Map;
019 import java.util.regex.Matcher;
020 import java.util.regex.Pattern;
021 import java.util.regex.PatternSyntaxException;
022
023 import org.dellroad.stuff.java.Primitive;
024
025 /**
026 * Parses strings using regular expressions into new instances of some class by parsing substrings.
027 * Primitive and String values are handled automatically. Other property types can be handled by
028 * overriding {@link #setProperty}.
029 *
030 * <a name="namedgroup"/>
031 * <p>
032 * This class supports parsing using a <i>named group regular expression</i>, which is pattern string
033 * using normal {@link Pattern} regular expression syntax with one additional grouping construct of the
034 * form <code>({property}...)</code>, allowing the Java bean property name to be specified inside the
035 * curly braces at the start of a grouped subexpression.
036 *
037 * <p>
038 * Instances of this class are immutable and thread-safe.
039 */
040 public class SimpleObjectParser<T> {
041
042 private final Class<T> targetClass;
043 private final HashMap<String, PropertyDescriptor> propertyMap = new HashMap<String, PropertyDescriptor>();
044
045 /**
046 * Constructor.
047 *
048 * @param targetClass type of target object we will be parsing
049 */
050 public SimpleObjectParser(Class<T> targetClass) {
051 this.targetClass = targetClass;
052 this.buildPropertyMap();
053 }
054
055 /**
056 * Get the target class.
057 */
058 public Class<T> getTargetClass() {
059 return this.targetClass;
060 }
061
062 /**
063 * Same as {@link #parse(Object, String, String, boolean)} but this method creates the target instance using
064 * the target type's default constructor.
065 *
066 * @throws RuntimeException if a new target instance cannot be created using the default constructor
067 * @since 1.0.85
068 */
069 public T parse(String text, String regex, boolean allowSubstringMatch) {
070 T target;
071 try {
072 target = this.targetClass.newInstance();
073 } catch (Exception e) {
074 throw new RuntimeException("can't create instance of " + this.targetClass + " using default constructor", e);
075 }
076 return this.parse(target, text, regex, allowSubstringMatch);
077 }
078
079 /**
080 * Parse the given text using the provided <i>named group regular expression</i>.
081 *
082 * <p>
083 * This method assumes the following about {@code regex}:
084 * <ul>
085 * <li>All instances of an opening parenthesis not preceded by a backslash are actual grouped sub-expressions</li>
086 * <li>In particular, all instances of substrings like <code>({foo}</code> are actual named group sub-expressions</li>
087 * </ul>
088 *
089 * @param target target instance
090 * @param text string to parse
091 * @param regex named group regular expression containing object property names
092 * @param allowSubstringMatch if false, entire text must match, otherwise only a (the first) substring need match
093 * @return parsed object or null if parse fails
094 * @throws PatternSyntaxException if the regular expression with the named group property names removed is invalid
095 * @throws PatternSyntaxException if this method cannot successfully parse the regular expression
096 * @throws IllegalArgumentException if a named group specfies a property that is not a parseable
097 * property of this instance's target class
098 * @since 1.0.95
099 */
100 public T parse(T target, String text, String regex, boolean allowSubstringMatch) {
101
102 // Scan regular expression for named sub-groups and parse them out
103 HashMap<Integer, String> patternMap = new HashMap<Integer, String>();
104 StringBuilder buf = new StringBuilder(regex.length());
105 Pattern namedGroup = Pattern.compile("\\(\\{(\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)\\}");
106 Matcher matcher = namedGroup.matcher(regex);
107 int pos = 0;
108 int groupCount = 0;
109 while (true) {
110 int match = matcher.find(pos) ? matcher.start() : regex.length();
111 String chunk = regex.substring(pos, match);
112 for (int i = 0; i < chunk.length(); i++) {
113 if (chunk.charAt(i) == '('
114 && (i == 0 || chunk.charAt(i - 1) != '\\')
115 && (i == chunk.length() - 1 || chunk.charAt(i + 1) != '?'))
116 groupCount++;
117 }
118 buf.append(chunk);
119 if (match == regex.length())
120 break;
121 buf.append('(');
122 patternMap.put(++groupCount, matcher.group(1));
123 pos = matcher.end();
124 }
125
126 // Sanity check our parse attempt
127 Pattern pattern = Pattern.compile(buf.toString());
128 int numGroups = pattern.matcher("").groupCount();
129 if (numGroups != groupCount) {
130 throw new PatternSyntaxException("the given regular expression is not supported (counted "
131 + groupCount + " != " + numGroups + " groups)", regex, 0);
132 }
133
134 // Proceed
135 return this.parse(target, text, pattern, patternMap, allowSubstringMatch);
136 }
137
138 /**
139 * Same as {@link #parse(Object, String, Pattern, Map, boolean)} but this method creates the target instance using
140 * the target type's default constructor.
141 *
142 * @throws RuntimeException if a new target instance cannot be created using the default constructor
143 * @since 1.0.85
144 */
145 public T parse(String text, Pattern pattern, Map<Integer, String> patternMap, boolean allowSubstringMatch) {
146 T target;
147 try {
148 target = this.targetClass.newInstance();
149 } catch (Exception e) {
150 throw new RuntimeException("can't create instance of " + this.targetClass + " using default constructor", e);
151 }
152 return this.parse(target, text, pattern, patternMap, allowSubstringMatch);
153 }
154
155 /**
156 * Parse the given text using the provided pattern and mapping from pattern sub-group to Java bean property name.
157 *
158 * @param target target instance
159 * @param text string to parse
160 * @param pattern pattern with substring matching groups that match object properties
161 * @param patternMap mapping from pattern substring group index to object property name
162 * @param allowSubstringMatch if false, entire text must match, otherwise only a (the first) substring need match
163 * @return parsed object or null if parse fails
164 * @throws IllegalArgumentException if the map contains a property that is not a parseable
165 * property of this instance's target class
166 * @throws IllegalArgumentException if a subgroup index key in patternMap is out of bounds
167 * @since 1.0.95
168 */
169 public T parse(T target, String text, Pattern pattern, Map<Integer, String> patternMap, boolean allowSubstringMatch) {
170
171 // Compose given map with target class' property map
172 HashMap<Integer, PropertyDescriptor> subgroupMap = new HashMap<Integer, PropertyDescriptor>();
173 for (Map.Entry<Integer, String> entry : patternMap.entrySet()) {
174 String propName = entry.getValue();
175 PropertyDescriptor property = this.propertyMap.get(propName);
176 if (property == null)
177 throw new IllegalArgumentException("parseable property \"" + propName + "\" not found in " + this.targetClass);
178 subgroupMap.put(entry.getKey(), property);
179 }
180
181 // Attempt to match the string
182 Matcher matcher = pattern.matcher(text);
183 boolean matches = allowSubstringMatch ? matcher.find() : matcher.matches();
184 if (!matches)
185 return null;
186
187 // Set fields based on matching substrings
188 for (Map.Entry<Integer, PropertyDescriptor> entry : subgroupMap.entrySet()) {
189
190 // Get substring
191 String substring;
192 try {
193 substring = matcher.group(entry.getKey());
194 } catch (IndexOutOfBoundsException e) {
195 throw new IllegalArgumentException(
196 "regex subgroup " + entry.getKey() + " does not exist in pattern `" + pattern + "'");
197 }
198
199 // If substring was not matched, don't set property
200 if (substring == null)
201 continue;
202
203 // Set property from substring
204 this.setProperty(target, entry.getValue(), substring);
205 }
206
207 // Post-process
208 this.postProcess(target);
209
210 // Done
211 return target;
212 }
213
214 /**
215 * Get the mapping from property name to setter method.
216 */
217 public Map<String, PropertyDescriptor> getPropertyMap() {
218 return Collections.unmodifiableMap(this.propertyMap);
219 }
220
221 /**
222 * Set a property value.
223 * <p/>
224 * <p>
225 * The implementation in {@link SimpleObjectParser} simply invokes {@link #setSimpleProperty}.
226 * Other property types can be handled by overriding this method.
227 * </p>
228 *
229 * @param obj newly created instance
230 * @param property descriptor for the property being set
231 * @param substring matched substring
232 * @throws IllegalArgumentException if substring cannot be successfully parsed
233 * @throws IllegalArgumentException if an exception is thrown attempting to set the property
234 */
235 public void setProperty(T obj, PropertyDescriptor property, String substring) {
236 this.setSimpleProperty(obj, property, substring);
237 }
238
239 /**
240 * Set a primitive or string property value.
241 * <p/>
242 * <p>
243 * The implementation in {@link SimpleObjectParser} handles primitives using the corresponding
244 * {@code valueOf} method; String values are handled by setting the value directly.
245 * </p>
246 *
247 * @throws IllegalArgumentException if property is not a primitive or String property.
248 * @throws IllegalArgumentException if substring cannot be successfully parsed (if primitive)
249 * @throws IllegalArgumentException if an exception is thrown attempting to set the property
250 */
251 public void setSimpleProperty(T obj, PropertyDescriptor property, String substring) {
252
253 // Parse substring
254 Object value;
255 if (property.getPropertyType() == String.class)
256 value = substring;
257 else {
258 Primitive prim = Primitive.get(property.getPropertyType());
259 if (prim == null) {
260 throw new IllegalArgumentException(
261 "property `" + property.getName() + "' of " + this.targetClass + " is not a primitive or String");
262 }
263 value = prim.parseValue(substring);
264 }
265
266 // Set value
267 try {
268 property.getWriteMethod().invoke(obj, value);
269 } catch (Exception e) {
270 throw new IllegalArgumentException("can't set property `" + property.getName() + "' of " + this.targetClass, e);
271 }
272 }
273
274 /**
275 * Post-process newly created instances. The instance's properties will have already been
276 * set by a successful parse.
277 * <p/>
278 * <p>
279 * The implementation in {@link SimpleObjectParser} does nothing. Subclasses may override if needed.
280 * </p>
281 */
282 protected void postProcess(T obj) {
283 }
284
285 private void buildPropertyMap() {
286
287 // Introspect target class
288 BeanInfo beanInfo;
289 try {
290 beanInfo = Introspector.getBeanInfo(this.targetClass);
291 } catch (IntrospectionException e) {
292 throw new RuntimeException(e);
293 }
294
295 // Build map from property name -> setter method
296 for (PropertyDescriptor property : beanInfo.getPropertyDescriptors()) {
297 if (property instanceof IndexedPropertyDescriptor)
298 continue;
299 Method setter = property.getWriteMethod();
300 if (setter == null)
301 continue;
302 Class<?> type = property.getPropertyType();
303 this.propertyMap.put(property.getName(), property);
304 }
305 }
306 }
307