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.ClassUtils;
026    import org.apache.shiro.util.CollectionUtils;
027    import org.apache.shiro.util.Nameable;
028    import org.apache.shiro.util.StringUtils;
029    import org.slf4j.Logger;
030    import org.slf4j.LoggerFactory;
031    
032    import java.beans.PropertyDescriptor;
033    import java.util.*;
034    
035    
036    /**
037     * Object builder that uses reflection and Apache Commons BeanUtils to build objects given a
038     * map of "property values".  Typically these come from the Shiro INI configuration and are used
039     * to construct or modify the SecurityManager, its dependencies, and web-based security filters.
040     *
041     * @author Les Hazlewood
042     * @author Jeremy Haile
043     * @since 0.9
044     */
045    public class ReflectionBuilder {
046    
047        //TODO - complete JavaDoc
048    
049        private static final Logger log = LoggerFactory.getLogger(ReflectionBuilder.class);
050    
051        private static final String OBJECT_REFERENCE_BEGIN_TOKEN = "$";
052        private static final String ESCAPED_OBJECT_REFERENCE_BEGIN_TOKEN = "\\$";
053        private static final String GLOBAL_PROPERTY_PREFIX = "shiro";
054        private static final char MAP_KEY_VALUE_DELIMITER = ':';
055        private static final String HEX_BEGIN_TOKEN = "0x";
056    
057        private Map<String, ?> objects;
058    
059        public ReflectionBuilder() {
060            this.objects = new LinkedHashMap<String, Object>();
061        }
062    
063        public ReflectionBuilder(Map<String, ?> defaults) {
064            this.objects = CollectionUtils.isEmpty(defaults) ? new LinkedHashMap<String, Object>() : defaults;
065        }
066    
067        public Map<String, ?> getObjects() {
068            return objects;
069        }
070    
071        public void setObjects(Map<String, ?> objects) {
072            this.objects = CollectionUtils.isEmpty(objects) ? new LinkedHashMap<String, Object>() : objects;
073        }
074    
075        public Object getBean(String id) {
076            return objects.get(id);
077        }
078    
079        @SuppressWarnings({"unchecked"})
080        public <T> T getBean(String id, Class<T> requiredType) {
081            if (requiredType == null) {
082                throw new NullPointerException("requiredType argument cannot be null.");
083            }
084            Object bean = getBean(id);
085            if (bean == null) {
086                return null;
087            }
088            if (!requiredType.isAssignableFrom(bean.getClass())) {
089                throw new IllegalStateException("Bean with id [" + id + "] is not of the required type [" +
090                        requiredType.getName() + "].");
091            }
092            return (T) bean;
093        }
094    
095        @SuppressWarnings({"unchecked"})
096        public Map<String, ?> buildObjects(Map<String, String> kvPairs) {
097            if (kvPairs != null && !kvPairs.isEmpty()) {
098    
099                // Separate key value pairs into object declarations and property assignment
100                // so that all objects can be created up front
101    
102                //https://issues.apache.org/jira/browse/SHIRO-85 - need to use LinkedHashMaps here:
103                Map<String, String> instanceMap = new LinkedHashMap<String, String>();
104                Map<String, String> propertyMap = new LinkedHashMap<String, String>();
105    
106                for (Map.Entry<String, String> entry : kvPairs.entrySet()) {
107                    if (entry.getKey().indexOf('.') < 0 || entry.getKey().endsWith(".class")) {
108                        instanceMap.put(entry.getKey(), entry.getValue());
109                    } else {
110                        propertyMap.put(entry.getKey(), entry.getValue());
111                    }
112                }
113    
114                // Create all instances
115                for (Map.Entry<String, String> entry : instanceMap.entrySet()) {
116                    createNewInstance((Map<String, Object>) objects, entry.getKey(), entry.getValue());
117                }
118    
119                // Set all properties
120                for (Map.Entry<String, String> entry : propertyMap.entrySet()) {
121                    applyProperty(entry.getKey(), entry.getValue(), objects);
122                }
123            }
124    
125            return objects;
126        }
127    
128        protected void createNewInstance(Map<String, Object> objects, String name, String value) {
129    
130            Object currentInstance = objects.get(name);
131            if (currentInstance != null) {
132                log.info("An instance with name '{}' already exists.  " +
133                        "Redefining this object as a new instance of type []", name, value);
134            }
135    
136            Object instance;//name with no property, assume right hand side of equals sign is the class name:
137            try {
138                instance = ClassUtils.newInstance(value);
139                if (instance instanceof Nameable) {
140                    ((Nameable) instance).setName(name);
141                }
142            } catch (Exception e) {
143                String msg = "Unable to instantiate class [" + value + "] for object named '" + name + "'.  " +
144                        "Please ensure you've specified the fully qualified class name correctly.";
145                throw new ConfigurationException(msg, e);
146            }
147            objects.put(name, instance);
148        }
149    
150        protected void applyProperty(String key, String value, Map objects) {
151    
152            int index = key.indexOf('.');
153    
154            if (index >= 0) {
155                String name = key.substring(0, index);
156                String property = key.substring(index + 1, key.length());
157    
158                if (GLOBAL_PROPERTY_PREFIX.equalsIgnoreCase(name)) {
159                    applyGlobalProperty(objects, property, value);
160                } else {
161                    applySingleProperty(objects, name, property, value);
162                }
163    
164            } else {
165                throw new IllegalArgumentException("All property keys must contain a '.' character. " +
166                        "(e.g. myBean.property = value)  These should already be separated out by buildObjects().");
167            }
168        }
169    
170        protected void applyGlobalProperty(Map objects, String property, String value) {
171            for (Object instance : objects.values()) {
172                try {
173                    PropertyDescriptor pd = PropertyUtils.getPropertyDescriptor(instance, property);
174                    if (pd != null) {
175                        applyProperty(instance, property, value);
176                    }
177                } catch (Exception e) {
178                    String msg = "Error retrieving property descriptor for instance " +
179                            "of type [" + instance.getClass().getName() + "] " +
180                            "while setting property [" + property + "]";
181                    throw new ConfigurationException(msg, e);
182                }
183            }
184        }
185    
186        protected void applySingleProperty(Map objects, String name, String property, String value) {
187            Object instance = objects.get(name);
188            if (property.equals("class")) {
189                throw new IllegalArgumentException("Property keys should not contain 'class' properties since these " +
190                        "should already be separated out by buildObjects().");
191    
192            } else if (instance == null) {
193                String msg = "Configuration error.  Specified object [" + name + "] with property [" +
194                        property + "] without first defining that object's class.  Please first " +
195                        "specify the class property first, e.g. myObject = fully_qualified_class_name " +
196                        "and then define additional properties.";
197                throw new IllegalArgumentException(msg);
198    
199            } else {
200                applyProperty(instance, property, value);
201            }
202        }
203    
204        protected boolean isReference(String value) {
205            return value != null && value.startsWith(OBJECT_REFERENCE_BEGIN_TOKEN);
206        }
207    
208        protected String getId(String referenceToken) {
209            return referenceToken.substring(OBJECT_REFERENCE_BEGIN_TOKEN.length());
210        }
211    
212        protected Object getReferencedObject(String id) {
213            Object o = objects != null && !objects.isEmpty() ? objects.get(id) : null;
214            if (o == null) {
215                String msg = "The object with id [" + id + "] has not yet been defined and therefore cannot be " +
216                        "referenced.  Please ensure objects are defined in the order in which they should be " +
217                        "created and made available for future reference.";
218                throw new UnresolveableReferenceException(msg);
219            }
220            return o;
221        }
222    
223        protected String unescapeIfNecessary(String value) {
224            if (value != null && value.startsWith(ESCAPED_OBJECT_REFERENCE_BEGIN_TOKEN)) {
225                return value.substring(ESCAPED_OBJECT_REFERENCE_BEGIN_TOKEN.length() - 1);
226            }
227            return value;
228        }
229    
230        protected Object resolveReference(String reference) {
231            String id = getId(reference);
232            log.debug("Encountered object reference '{}'.  Looking up object with id '{}'", reference, id);
233            return getReferencedObject(id);
234        }
235    
236        protected boolean isTypedProperty(Object object, String propertyName, Class clazz) {
237            if (clazz == null) {
238                throw new NullPointerException("type (class) argument cannot be null.");
239            }
240            try {
241                PropertyDescriptor descriptor = PropertyUtils.getPropertyDescriptor(object, propertyName);
242                if (descriptor == null) {
243                    String msg = "Property '" + propertyName + "' does not exist for object of " +
244                            "type " + object.getClass().getName() + ".";
245                    throw new ConfigurationException(msg);
246                }
247                Class propertyClazz = descriptor.getPropertyType();
248                return clazz.isAssignableFrom(propertyClazz);
249            } catch (ConfigurationException ce) {
250                //let it propagate:
251                throw ce;
252            } catch (Exception e) {
253                String msg = "Unable to determine if property [" + propertyName + "] represents a " + clazz.getName();
254                throw new ConfigurationException(msg, e);
255            }
256        }
257    
258        protected Set<?> toSet(String sValue) {
259            String[] tokens = StringUtils.split(sValue);
260            if (tokens == null || tokens.length <= 0) {
261                return null;
262            }
263            Set<String> setTokens = new LinkedHashSet<String>(Arrays.asList(tokens));
264    
265            //now convert into correct values and/or references:
266            Set<Object> values = new LinkedHashSet<Object>(setTokens.size());
267            for (String token : setTokens) {
268                Object value = resolveValue(token);
269                values.add(value);
270            }
271            return values;
272        }
273    
274        protected Map<?, ?> toMap(String sValue) {
275            String[] tokens = StringUtils.split(sValue, StringUtils.DEFAULT_DELIMITER_CHAR,
276                    StringUtils.DEFAULT_QUOTE_CHAR, StringUtils.DEFAULT_QUOTE_CHAR, true, true);
277            if (tokens == null || tokens.length <= 0) {
278                return null;
279            }
280    
281            Map<String, String> mapTokens = new LinkedHashMap<String, String>(tokens.length);
282            for (String token : tokens) {
283                String[] kvPair = StringUtils.split(token, MAP_KEY_VALUE_DELIMITER);
284                if (kvPair == null || kvPair.length != 2) {
285                    String msg = "Map property value [" + sValue + "] contained key-value pair token [" +
286                            token + "] that does not properly split to a single key and pair.  This must be the " +
287                            "case for all map entries.";
288                    throw new ConfigurationException(msg);
289                }
290                mapTokens.put(kvPair[0], kvPair[1]);
291            }
292    
293            //now convert into correct values and/or references:
294            Map<Object, Object> map = new LinkedHashMap<Object, Object>(mapTokens.size());
295            for (Map.Entry<String, String> entry : mapTokens.entrySet()) {
296                Object key = resolveValue(entry.getKey());
297                Object value = resolveValue(entry.getValue());
298                map.put(key, value);
299            }
300            return map;
301        }
302    
303    
304        protected List<?> toList(String sValue) {
305            String[] tokens = StringUtils.split(sValue);
306            if (tokens == null || tokens.length <= 0) {
307                return null;
308            }
309    
310            //now convert into correct values and/or references:
311            List<Object> values = new ArrayList<Object>(tokens.length);
312            for (String token : tokens) {
313                Object value = resolveValue(token);
314                values.add(value);
315            }
316            return values;
317        }
318    
319        protected byte[] toBytes(String sValue) {
320            if (sValue == null) {
321                return null;
322            }
323            byte[] bytes;
324            if (sValue.startsWith(HEX_BEGIN_TOKEN)) {
325                String hex = sValue.substring(HEX_BEGIN_TOKEN.length());
326                bytes = Hex.decode(hex);
327            } else {
328                //assume base64 encoded:
329                bytes = Base64.decode(sValue);
330            }
331            return bytes;
332        }
333    
334        protected Object resolveValue(String stringValue) {
335            Object value;
336            if (isReference(stringValue)) {
337                value = resolveReference(stringValue);
338            } else {
339                value = unescapeIfNecessary(stringValue);
340            }
341            return value;
342        }
343    
344    
345        protected void applyProperty(Object object, String propertyName, String stringValue) {
346    
347            Object value;
348    
349            if (isTypedProperty(object, propertyName, Set.class)) {
350                value = toSet(stringValue);
351            } else if (isTypedProperty(object, propertyName, Map.class)) {
352                value = toMap(stringValue);
353            } else if (isTypedProperty(object, propertyName, List.class) ||
354                    isTypedProperty(object, propertyName, Collection.class)) {
355                value = toList(stringValue);
356            } else if (isTypedProperty(object, propertyName, byte[].class)) {
357                value = toBytes(stringValue);
358            } else {
359                value = resolveValue(stringValue);
360            }
361    
362            try {
363                if (log.isTraceEnabled()) {
364                    log.trace("Applying property [{}] value [{}] on object of type [{}]",
365                            new Object[]{propertyName, value, object.getClass().getName()});
366                }
367                BeanUtils.setProperty(object, propertyName, value);
368            } catch (Exception e) {
369                String msg = "Unable to set property '" + propertyName + "' with value [" + stringValue + "] on object " +
370                        "of type " + (object != null ? object.getClass().getName() : null) + ".  If " +
371                        "'" + stringValue + "' is a reference to another (previously defined) object, prefix it with " +
372                        "'" + OBJECT_REFERENCE_BEGIN_TOKEN + "' to indicate that the referenced " +
373                        "object should be used as the actual value.  " +
374                        "For example, " + OBJECT_REFERENCE_BEGIN_TOKEN + stringValue;
375                throw new ConfigurationException(msg, e);
376            }
377        }
378    
379    }