001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.shiro.config;
020
021import org.apache.commons.beanutils.BeanUtils;
022import org.apache.commons.beanutils.PropertyUtils;
023import org.apache.shiro.codec.Base64;
024import org.apache.shiro.codec.Hex;
025import org.apache.shiro.util.*;
026import org.slf4j.Logger;
027import org.slf4j.LoggerFactory;
028
029import java.beans.PropertyDescriptor;
030import java.util.*;
031
032
033/**
034 * Object builder that uses reflection and Apache Commons BeanUtils to build objects given a
035 * map of "property values".  Typically these come from the Shiro INI configuration and are used
036 * to construct or modify the SecurityManager, its dependencies, and web-based security filters.
037 * <p/>
038 * Recognizes {@link Factory} implementations and will call
039 * {@link org.apache.shiro.util.Factory#getInstance() getInstance} to satisfy any reference to this bean.
040 *
041 * @since 0.9
042 */
043public class ReflectionBuilder {
044
045    //TODO - complete JavaDoc
046
047    private static final Logger log = LoggerFactory.getLogger(ReflectionBuilder.class);
048
049    private static final String OBJECT_REFERENCE_BEGIN_TOKEN = "$";
050    private static final String ESCAPED_OBJECT_REFERENCE_BEGIN_TOKEN = "\\$";
051    private static final String GLOBAL_PROPERTY_PREFIX = "shiro";
052    private static final char MAP_KEY_VALUE_DELIMITER = ':';
053    private static final String HEX_BEGIN_TOKEN = "0x";
054    private static final String NULL_VALUE_TOKEN = "null";
055    private static final String EMPTY_STRING_VALUE_TOKEN = "\"\"";
056    private static final char STRING_VALUE_DELIMETER = '"';
057    private static final char MAP_PROPERTY_BEGIN_TOKEN = '[';
058    private static final char MAP_PROPERTY_END_TOKEN = ']';
059
060    private Map<String, ?> objects;
061
062    public ReflectionBuilder() {
063        this.objects = new LinkedHashMap<String, Object>();
064    }
065
066    public ReflectionBuilder(Map<String, ?> defaults) {
067        this.objects = CollectionUtils.isEmpty(defaults) ? new LinkedHashMap<String, Object>() : defaults;
068    }
069
070    public Map<String, ?> getObjects() {
071        return objects;
072    }
073
074    public void setObjects(Map<String, ?> objects) {
075        this.objects = CollectionUtils.isEmpty(objects) ? new LinkedHashMap<String, Object>() : objects;
076    }
077
078    public Object getBean(String id) {
079        return objects.get(id);
080    }
081
082    @SuppressWarnings({"unchecked"})
083    public <T> T getBean(String id, Class<T> requiredType) {
084        if (requiredType == null) {
085            throw new NullPointerException("requiredType argument cannot be null.");
086        }
087        Object bean = getBean(id);
088        if (bean == null) {
089            return null;
090        }
091        if (!requiredType.isAssignableFrom(bean.getClass())) {
092            throw new IllegalStateException("Bean with id [" + id + "] is not of the required type [" +
093                    requiredType.getName() + "].");
094        }
095        return (T) bean;
096    }
097
098    @SuppressWarnings({"unchecked"})
099    public Map<String, ?> buildObjects(Map<String, String> kvPairs) {
100        if (kvPairs != null && !kvPairs.isEmpty()) {
101
102            // Separate key value pairs into object declarations and property assignment
103            // so that all objects can be created up front
104
105            //https://issues.apache.org/jira/browse/SHIRO-85 - need to use LinkedHashMaps here:
106            Map<String, String> instanceMap = new LinkedHashMap<String, String>();
107            Map<String, String> propertyMap = new LinkedHashMap<String, String>();
108
109            for (Map.Entry<String, String> entry : kvPairs.entrySet()) {
110                if (entry.getKey().indexOf('.') < 0 || entry.getKey().endsWith(".class")) {
111                    instanceMap.put(entry.getKey(), entry.getValue());
112                } else {
113                    propertyMap.put(entry.getKey(), entry.getValue());
114                }
115            }
116
117            // Create all instances
118            for (Map.Entry<String, String> entry : instanceMap.entrySet()) {
119                createNewInstance((Map<String, Object>) objects, entry.getKey(), entry.getValue());
120            }
121
122            // Set all properties
123            for (Map.Entry<String, String> entry : propertyMap.entrySet()) {
124                applyProperty(entry.getKey(), entry.getValue(), objects);
125            }
126        }
127
128        //SHIRO-413: init method must be called for constructed objects that are Initializable
129        LifecycleUtils.init(objects.values());
130
131        return objects;
132    }
133
134    protected void createNewInstance(Map<String, Object> objects, String name, String value) {
135
136        Object currentInstance = objects.get(name);
137        if (currentInstance != null) {
138            log.info("An instance with name '{}' already exists.  " +
139                    "Redefining this object as a new instance of type {}", name, value);
140        }
141
142        Object instance;//name with no property, assume right hand side of equals sign is the class name:
143        try {
144            instance = ClassUtils.newInstance(value);
145            if (instance instanceof Nameable) {
146                ((Nameable) instance).setName(name);
147            }
148        } catch (Exception e) {
149            String msg = "Unable to instantiate class [" + value + "] for object named '" + name + "'.  " +
150                    "Please ensure you've specified the fully qualified class name correctly.";
151            throw new ConfigurationException(msg, e);
152        }
153        objects.put(name, instance);
154    }
155
156    protected void applyProperty(String key, String value, Map objects) {
157
158        int index = key.indexOf('.');
159
160        if (index >= 0) {
161            String name = key.substring(0, index);
162            String property = key.substring(index + 1, key.length());
163
164            if (GLOBAL_PROPERTY_PREFIX.equalsIgnoreCase(name)) {
165                applyGlobalProperty(objects, property, value);
166            } else {
167                applySingleProperty(objects, name, property, value);
168            }
169
170        } else {
171            throw new IllegalArgumentException("All property keys must contain a '.' character. " +
172                    "(e.g. myBean.property = value)  These should already be separated out by buildObjects().");
173        }
174    }
175
176    protected void applyGlobalProperty(Map objects, String property, String value) {
177        for (Object instance : objects.values()) {
178            try {
179                PropertyDescriptor pd = PropertyUtils.getPropertyDescriptor(instance, property);
180                if (pd != null) {
181                    applyProperty(instance, property, value);
182                }
183            } catch (Exception e) {
184                String msg = "Error retrieving property descriptor for instance " +
185                        "of type [" + instance.getClass().getName() + "] " +
186                        "while setting property [" + property + "]";
187                throw new ConfigurationException(msg, e);
188            }
189        }
190    }
191
192    protected void applySingleProperty(Map objects, String name, String property, String value) {
193        Object instance = objects.get(name);
194        if (property.equals("class")) {
195            throw new IllegalArgumentException("Property keys should not contain 'class' properties since these " +
196                    "should already be separated out by buildObjects().");
197
198        } else if (instance == null) {
199            String msg = "Configuration error.  Specified object [" + name + "] with property [" +
200                    property + "] without first defining that object's class.  Please first " +
201                    "specify the class property first, e.g. myObject = fully_qualified_class_name " +
202                    "and then define additional properties.";
203            throw new IllegalArgumentException(msg);
204
205        } else {
206            applyProperty(instance, property, value);
207        }
208    }
209
210    protected boolean isReference(String value) {
211        return value != null && value.startsWith(OBJECT_REFERENCE_BEGIN_TOKEN);
212    }
213
214    protected String getId(String referenceToken) {
215        return referenceToken.substring(OBJECT_REFERENCE_BEGIN_TOKEN.length());
216    }
217
218    protected Object getReferencedObject(String id) {
219        Object o = objects != null && !objects.isEmpty() ? objects.get(id) : null;
220        if (o == null) {
221            String msg = "The object with id [" + id + "] has not yet been defined and therefore cannot be " +
222                    "referenced.  Please ensure objects are defined in the order in which they should be " +
223                    "created and made available for future reference.";
224            throw new UnresolveableReferenceException(msg);
225        }
226        return o;
227    }
228
229    protected String unescapeIfNecessary(String value) {
230        if (value != null && value.startsWith(ESCAPED_OBJECT_REFERENCE_BEGIN_TOKEN)) {
231            return value.substring(ESCAPED_OBJECT_REFERENCE_BEGIN_TOKEN.length() - 1);
232        }
233        return value;
234    }
235
236    protected Object resolveReference(String reference) {
237        String id = getId(reference);
238        log.debug("Encountered object reference '{}'.  Looking up object with id '{}'", reference, id);
239        final Object referencedObject = getReferencedObject(id);
240        if (referencedObject instanceof Factory) {
241            return ((Factory) referencedObject).getInstance();
242        }
243        return referencedObject;
244    }
245
246    protected boolean isTypedProperty(Object object, String propertyName, Class clazz) {
247        if (clazz == null) {
248            throw new NullPointerException("type (class) argument cannot be null.");
249        }
250        try {
251            PropertyDescriptor descriptor = PropertyUtils.getPropertyDescriptor(object, propertyName);
252            if (descriptor == null) {
253                String msg = "Property '" + propertyName + "' does not exist for object of " +
254                        "type " + object.getClass().getName() + ".";
255                throw new ConfigurationException(msg);
256            }
257            Class propertyClazz = descriptor.getPropertyType();
258            return clazz.isAssignableFrom(propertyClazz);
259        } catch (ConfigurationException ce) {
260            //let it propagate:
261            throw ce;
262        } catch (Exception e) {
263            String msg = "Unable to determine if property [" + propertyName + "] represents a " + clazz.getName();
264            throw new ConfigurationException(msg, e);
265        }
266    }
267
268    protected Set<?> toSet(String sValue) {
269        String[] tokens = StringUtils.split(sValue);
270        if (tokens == null || tokens.length <= 0) {
271            return null;
272        }
273
274        //SHIRO-423: check to see if the value is a referenced Set already, and if so, return it immediately:
275        if (tokens.length == 1 && isReference(tokens[0])) {
276            Object reference = resolveReference(tokens[0]);
277            if (reference instanceof Set) {
278                return (Set)reference;
279            }
280        }
281
282        Set<String> setTokens = new LinkedHashSet<String>(Arrays.asList(tokens));
283
284        //now convert into correct values and/or references:
285        Set<Object> values = new LinkedHashSet<Object>(setTokens.size());
286        for (String token : setTokens) {
287            Object value = resolveValue(token);
288            values.add(value);
289        }
290        return values;
291    }
292
293    protected Map<?, ?> toMap(String sValue) {
294        String[] tokens = StringUtils.split(sValue, StringUtils.DEFAULT_DELIMITER_CHAR,
295                StringUtils.DEFAULT_QUOTE_CHAR, StringUtils.DEFAULT_QUOTE_CHAR, true, true);
296        if (tokens == null || tokens.length <= 0) {
297            return null;
298        }
299
300        //SHIRO-423: check to see if the value is a referenced Map already, and if so, return it immediately:
301        if (tokens.length == 1 && isReference(tokens[0])) {
302            Object reference = resolveReference(tokens[0]);
303            if (reference instanceof Map) {
304                return (Map)reference;
305            }
306        }
307
308        Map<String, String> mapTokens = new LinkedHashMap<String, String>(tokens.length);
309        for (String token : tokens) {
310            String[] kvPair = StringUtils.split(token, MAP_KEY_VALUE_DELIMITER);
311            if (kvPair == null || kvPair.length != 2) {
312                String msg = "Map property value [" + sValue + "] contained key-value pair token [" +
313                        token + "] that does not properly split to a single key and pair.  This must be the " +
314                        "case for all map entries.";
315                throw new ConfigurationException(msg);
316            }
317            mapTokens.put(kvPair[0], kvPair[1]);
318        }
319
320        //now convert into correct values and/or references:
321        Map<Object, Object> map = new LinkedHashMap<Object, Object>(mapTokens.size());
322        for (Map.Entry<String, String> entry : mapTokens.entrySet()) {
323            Object key = resolveValue(entry.getKey());
324            Object value = resolveValue(entry.getValue());
325            map.put(key, value);
326        }
327        return map;
328    }
329
330    // @since 1.2.2
331    // TODO: make protected in 1.3+
332    private Collection<?> toCollection(String sValue) {
333
334        String[] tokens = StringUtils.split(sValue);
335        if (tokens == null || tokens.length <= 0) {
336            return null;
337        }
338
339        //SHIRO-423: check to see if the value is a referenced Collection already, and if so, return it immediately:
340        if (tokens.length == 1 && isReference(tokens[0])) {
341            Object reference = resolveReference(tokens[0]);
342            if (reference instanceof Collection) {
343                return (Collection)reference;
344            }
345        }
346
347        //now convert into correct values and/or references:
348        List<Object> values = new ArrayList<Object>(tokens.length);
349        for (String token : tokens) {
350            Object value = resolveValue(token);
351            values.add(value);
352        }
353        return values;
354    }
355
356    protected List<?> toList(String sValue) {
357        String[] tokens = StringUtils.split(sValue);
358        if (tokens == null || tokens.length <= 0) {
359            return null;
360        }
361
362        //SHIRO-423: check to see if the value is a referenced List already, and if so, return it immediately:
363        if (tokens.length == 1 && isReference(tokens[0])) {
364            Object reference = resolveReference(tokens[0]);
365            if (reference instanceof List) {
366                return (List)reference;
367            }
368        }
369
370        //now convert into correct values and/or references:
371        List<Object> values = new ArrayList<Object>(tokens.length);
372        for (String token : tokens) {
373            Object value = resolveValue(token);
374            values.add(value);
375        }
376        return values;
377    }
378
379    protected byte[] toBytes(String sValue) {
380        if (sValue == null) {
381            return null;
382        }
383        byte[] bytes;
384        if (sValue.startsWith(HEX_BEGIN_TOKEN)) {
385            String hex = sValue.substring(HEX_BEGIN_TOKEN.length());
386            bytes = Hex.decode(hex);
387        } else {
388            //assume base64 encoded:
389            bytes = Base64.decode(sValue);
390        }
391        return bytes;
392    }
393
394    protected Object resolveValue(String stringValue) {
395        Object value;
396        if (isReference(stringValue)) {
397            value = resolveReference(stringValue);
398        } else {
399            value = unescapeIfNecessary(stringValue);
400        }
401        return value;
402    }
403
404    protected String checkForNullOrEmptyLiteral(String stringValue) {
405        if (stringValue == null) {
406            return null;
407        }
408        //check if the value is the actual literal string 'null' (expected to be wrapped in quotes):
409        if (stringValue.equals("\"null\"")) {
410            return NULL_VALUE_TOKEN;
411        }
412        //or the actual literal string of two quotes '""' (expected to be wrapped in quotes):
413        else if (stringValue.equals("\"\"\"\"")) {
414            return EMPTY_STRING_VALUE_TOKEN;
415        } else {
416            return stringValue;
417        }
418    }
419    
420    protected void applyProperty(Object object, String propertyPath, Object value) {
421
422        int mapBegin = propertyPath.indexOf(MAP_PROPERTY_BEGIN_TOKEN);
423        int mapEnd = -1;
424        String mapPropertyPath = null;
425        String keyString = null;
426
427        String remaining = null;
428        
429        if (mapBegin >= 0) {
430            //a map is being referenced in the overall property path.  Find just the map's path:
431            mapPropertyPath = propertyPath.substring(0, mapBegin);
432            //find the end of the map reference:
433            mapEnd = propertyPath.indexOf(MAP_PROPERTY_END_TOKEN, mapBegin);
434            //find the token in between the [ and the ] (the map/array key or index):
435            keyString = propertyPath.substring(mapBegin+1, mapEnd);
436
437            //find out if there is more path reference to follow.  If not, we're at a terminal of the OGNL expression
438            if (propertyPath.length() > (mapEnd+1)) {
439                remaining = propertyPath.substring(mapEnd+1);
440                if (remaining.startsWith(".")) {
441                    remaining = StringUtils.clean(remaining.substring(1));
442                }
443            }
444        }
445        
446        if (remaining == null) {
447            //we've terminated the OGNL expression.  Check to see if we're assigning a property or a map entry:
448            if (keyString == null) {
449                //not a map or array value assignment - assign the property directly:
450                setProperty(object, propertyPath, value);
451            } else {
452                //we're assigning a map or array entry.  Check to see which we should call:
453                if (isTypedProperty(object, mapPropertyPath, Map.class)) {
454                    Map map = (Map)getProperty(object, mapPropertyPath);
455                    Object mapKey = resolveValue(keyString);
456                    //noinspection unchecked
457                    map.put(mapKey, value);
458                } else {
459                    //must be an array property.  Convert the key string to an index:
460                    int index = Integer.valueOf(keyString);
461                    setIndexedProperty(object, mapPropertyPath, index, value);
462                }
463            }
464        } else {
465            //property is being referenced as part of a nested path.  Find the referenced map/array entry and
466            //recursively call this method with the remaining property path
467            Object referencedValue = null;
468            if (isTypedProperty(object, mapPropertyPath, Map.class)) {
469                Map map = (Map)getProperty(object, mapPropertyPath);
470                Object mapKey = resolveValue(keyString);
471                referencedValue = map.get(mapKey);
472            } else {
473                //must be an array property:
474                int index = Integer.valueOf(keyString);
475                referencedValue = getIndexedProperty(object, mapPropertyPath, index);
476            }
477
478            if (referencedValue == null) {
479                throw new ConfigurationException("Referenced map/array value '" + mapPropertyPath + "[" +
480                keyString + "]' does not exist.");
481            }
482
483            applyProperty(referencedValue, remaining, value);
484        }
485    }
486    
487    private void setProperty(Object object, String propertyPath, Object value) {
488        try {
489            if (log.isTraceEnabled()) {
490                log.trace("Applying property [{}] value [{}] on object of type [{}]",
491                        new Object[]{propertyPath, value, object.getClass().getName()});
492            }
493            BeanUtils.setProperty(object, propertyPath, value);
494        } catch (Exception e) {
495            String msg = "Unable to set property '" + propertyPath + "' with value [" + value + "] on object " +
496                    "of type " + (object != null ? object.getClass().getName() : null) + ".  If " +
497                    "'" + value + "' is a reference to another (previously defined) object, prefix it with " +
498                    "'" + OBJECT_REFERENCE_BEGIN_TOKEN + "' to indicate that the referenced " +
499                    "object should be used as the actual value.  " +
500                    "For example, " + OBJECT_REFERENCE_BEGIN_TOKEN + value;
501            throw new ConfigurationException(msg, e);
502        }
503    }
504    
505    private Object getProperty(Object object, String propertyPath) {
506        try {
507            return PropertyUtils.getProperty(object, propertyPath);
508        } catch (Exception e) {
509            throw new ConfigurationException("Unable to access property '" + propertyPath + "'", e);
510        }
511    }
512    
513    private void setIndexedProperty(Object object, String propertyPath, int index, Object value) {
514        try {
515            PropertyUtils.setIndexedProperty(object, propertyPath, index, value);
516        } catch (Exception e) {
517            throw new ConfigurationException("Unable to set array property '" + propertyPath + "'", e);
518        }
519    }
520    
521    private Object getIndexedProperty(Object object, String propertyPath, int index) {
522        try {
523            return PropertyUtils.getIndexedProperty(object, propertyPath, index);
524        } catch (Exception e) {
525            throw new ConfigurationException("Unable to acquire array property '" + propertyPath + "'", e);
526        }
527    }
528    
529    protected boolean isIndexedPropertyAssignment(String propertyPath) {
530        return propertyPath.endsWith("" + MAP_PROPERTY_END_TOKEN);
531    }
532
533    protected void applyProperty(Object object, String propertyName, String stringValue) {
534
535        Object value;
536
537        if (NULL_VALUE_TOKEN.equals(stringValue)) {
538            value = null;
539        } else if (EMPTY_STRING_VALUE_TOKEN.equals(stringValue)) {
540            value = StringUtils.EMPTY_STRING;
541        } else if (isIndexedPropertyAssignment(propertyName)) {
542            String checked = checkForNullOrEmptyLiteral(stringValue);
543            value = resolveValue(checked);
544        } else if (isTypedProperty(object, propertyName, Set.class)) {
545            value = toSet(stringValue);
546        } else if (isTypedProperty(object, propertyName, Map.class)) {
547            value = toMap(stringValue);
548        } else if (isTypedProperty(object, propertyName, List.class)) {
549            value = toList(stringValue);
550        } else if (isTypedProperty(object, propertyName, Collection.class)) {
551            value = toCollection(stringValue);
552        } else if (isTypedProperty(object, propertyName, byte[].class)) {
553            value = toBytes(stringValue);
554        } else if (isTypedProperty(object, propertyName, ByteSource.class)) {
555            byte[] bytes = toBytes(stringValue);
556            value = ByteSource.Util.bytes(bytes);
557        } else {
558            String checked = checkForNullOrEmptyLiteral(stringValue);
559            value = resolveValue(checked);
560        }
561
562        applyProperty(object, propertyName, value);
563    }
564
565}