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 }