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     */
019    package org.apache.shiro.config;
020    
021    import org.apache.commons.beanutils.BeanUtils;
022    import org.apache.commons.beanutils.PropertyUtils;
023    import org.apache.shiro.codec.Base64;
024    import org.apache.shiro.codec.Hex;
025    import org.apache.shiro.util.*;
026    import org.slf4j.Logger;
027    import org.slf4j.LoggerFactory;
028    
029    import java.beans.PropertyDescriptor;
030    import 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     */
043    public 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    }