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