001 package org.apache.commons.betwixt.digester;
002
003 /*
004 * Licensed to the Apache Software Foundation (ASF) under one or more
005 * contributor license agreements. See the NOTICE file distributed with
006 * this work for additional information regarding copyright ownership.
007 * The ASF licenses this file to You under the Apache License, Version 2.0
008 * (the "License"); you may not use this file except in compliance with
009 * the License. You may obtain a copy of the License at
010 *
011 * http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 */
019 import java.beans.PropertyDescriptor;
020 import java.lang.reflect.Method;
021 import java.lang.reflect.Modifier;
022 import java.util.Map;
023
024 import org.apache.commons.betwixt.ElementDescriptor;
025 import org.apache.commons.betwixt.XMLBeanInfo;
026 import org.apache.commons.betwixt.XMLUtils;
027 import org.apache.commons.betwixt.expression.ConstantExpression;
028 import org.apache.commons.betwixt.expression.Expression;
029 import org.apache.commons.betwixt.expression.IteratorExpression;
030 import org.apache.commons.betwixt.expression.MethodExpression;
031 import org.apache.commons.betwixt.expression.MethodUpdater;
032 import org.apache.commons.logging.Log;
033 import org.apache.commons.logging.LogFactory;
034 import org.xml.sax.Attributes;
035 import org.xml.sax.SAXException;
036
037 /**
038 * <p>
039 * <code>ElementRule</code> the digester Rule for parsing the <element>
040 * elements.
041 * </p>
042 *
043 * @author <a href="mailto:jstrachan@apache.org">James Strachan</a>
044 */
045 public class ElementRule extends MappedPropertyRule {
046
047 /** Logger */
048 private static Log log = LogFactory.getLog(ElementRule.class);
049
050 /**
051 * Sets the log for this class
052 *
053 * @param newLog
054 * the new Log implementation for this class to use
055 * @since 0.5
056 */
057 public static final void setLog(Log newLog) {
058 log = newLog;
059 }
060
061 /** Class for which the .bewixt file is being digested */
062 private Class beanClass;
063
064 /** Base constructor */
065 public ElementRule() {
066 }
067
068 // Rule interface
069 // -------------------------------------------------------------------------
070
071 /**
072 * Process the beginning of this element.
073 *
074 * @param attributes
075 * The attribute list of this element
076 * @throws SAXException
077 * 1. If this tag's parent is not either an info or element tag.
078 * 2. If the name attribute is not valid XML element name. 3. If
079 * the name attribute is not present 4. If the class attribute
080 * is not a loadable (fully qualified) class name
081 */
082 public void begin(String name, String namespace, Attributes attributes)
083 throws SAXException {
084 String nameAttributeValue = attributes.getValue("name");
085
086 ElementDescriptor descriptor = new ElementDescriptor();
087 descriptor.setLocalName(nameAttributeValue);
088 String uri = attributes.getValue("uri");
089 String qName = nameAttributeValue;
090 if (uri != null && nameAttributeValue != null) {
091 descriptor.setURI(uri);
092 String prefix = getXMLIntrospector().getConfiguration()
093 .getPrefixMapper().getPrefix(uri);
094 qName = prefix + ":" + nameAttributeValue;
095 }
096 descriptor.setQualifiedName(qName);
097
098 String propertyName = attributes.getValue("property");
099 descriptor.setPropertyName(propertyName);
100
101 String propertyType = attributes.getValue("type");
102
103 if (log.isTraceEnabled()) {
104 log.trace("(BEGIN) name=" + nameAttributeValue + " uri=" + uri
105 + " property=" + propertyName + " type=" + propertyType);
106 }
107
108 // set mapping derivation
109 String mappingDerivation = attributes.getValue("mappingDerivation");
110 if ("introspection".equals(mappingDerivation)) {
111 descriptor.setUseBindTimeTypeForMapping(false);
112 } else if ("bind".equals(mappingDerivation)) {
113 descriptor.setUseBindTimeTypeForMapping(true);
114 }
115
116 // set the property type using reflection
117 descriptor.setPropertyType(getPropertyType(propertyType, beanClass,
118 propertyName));
119
120 boolean isCollective = getXMLIntrospector().getConfiguration()
121 .isLoopType(descriptor.getPropertyType());
122
123 descriptor.setCollective(isCollective);
124
125 // check that the name attribute is present
126 if (!isCollective
127 && (nameAttributeValue == null || nameAttributeValue.trim()
128 .equals(""))) {
129 // allow polymorphic mappings but log note for user
130 log
131 .info("No name attribute has been specified. This element will be polymorphic.");
132 }
133
134 // check that name is well formed
135 if (nameAttributeValue != null
136 && !XMLUtils.isWellFormedXMLName(nameAttributeValue)) {
137 throw new SAXException("'" + nameAttributeValue
138 + "' would not be a well formed xml element name.");
139 }
140
141 String implementationClass = attributes.getValue("class");
142 if (log.isTraceEnabled()) {
143 log.trace("'class' attribute=" + implementationClass);
144 }
145 if (implementationClass != null) {
146 try {
147
148 Class clazz = loadClass(implementationClass);
149 descriptor.setImplementationClass(clazz);
150
151 } catch (Exception e) {
152 if (log.isDebugEnabled()) {
153 log.debug(
154 "Cannot load class named: " + implementationClass,
155 e);
156 }
157 throw new SAXException("Cannot load class named: "
158 + implementationClass);
159 }
160 }
161
162 if (propertyName != null && propertyName.length() > 0) {
163 boolean forceAccessible = "true".equals(attributes
164 .getValue("forceAccessible"));
165 configureDescriptor(descriptor, attributes.getValue("updater"),
166 forceAccessible);
167
168 } else {
169 String value = attributes.getValue("value");
170 if (value != null) {
171 descriptor.setTextExpression(new ConstantExpression(value));
172 }
173 }
174
175 Object top = digester.peek();
176 if (top instanceof XMLBeanInfo) {
177 XMLBeanInfo beanInfo = (XMLBeanInfo) top;
178 beanInfo.setElementDescriptor(descriptor);
179 beanClass = beanInfo.getBeanClass();
180 descriptor.setPropertyType(beanClass);
181
182 } else if (top instanceof ElementDescriptor) {
183 ElementDescriptor parent = (ElementDescriptor) top;
184 parent.addElementDescriptor(descriptor);
185
186 } else {
187 throw new SAXException("Invalid use of <element>. It should "
188 + "be nested inside <info> or other <element> nodes");
189 }
190
191 digester.push(descriptor);
192 }
193
194 /**
195 * Process the end of this element.
196 */
197 public void end(String name, String namespace) {
198 ElementDescriptor descriptor = (ElementDescriptor)digester.pop();
199
200 final Object peek = digester.peek();
201
202 if(peek instanceof ElementDescriptor) {
203 ElementDescriptor parent = (ElementDescriptor)digester.peek();
204
205 // check for element suppression
206 if( getXMLIntrospector().getConfiguration().getElementSuppressionStrategy().suppress(descriptor)) {
207 parent.removeElementDescriptor(descriptor);
208 }
209 }
210 }
211
212 // Implementation methods
213 // -------------------------------------------------------------------------
214
215 /**
216 * Sets the Expression and Updater from a bean property name Uses the
217 * default updater (from the standard java bean property).
218 *
219 * @param elementDescriptor
220 * configure this <code>ElementDescriptor</code>
221 * @since 0.5
222 */
223 protected void configureDescriptor(ElementDescriptor elementDescriptor) {
224 configureDescriptor(elementDescriptor, null);
225 }
226
227 /**
228 * Sets the Expression and Updater from a bean property name Allows a custom
229 * updater to be passed in.
230 *
231 * @param elementDescriptor
232 * configure this <code>ElementDescriptor</code>
233 * @param updateMethodName
234 * custom update method. If null, then use standard
235 * @since 0.5
236 * @deprecated now calls
237 * <code>#configureDescriptor(ElementDescriptor, String, boolean)</code>
238 * which allow accessibility to be forced. The subclassing API
239 * was not really considered carefully when this class was
240 * created. If anyone subclasses this method please contact the
241 * mailing list and suitable hooks will be placed into the code.
242 */
243 protected void configureDescriptor(ElementDescriptor elementDescriptor,
244 String updateMethodName) {
245 configureDescriptor(elementDescriptor, null, false);
246 }
247
248 /**
249 * Sets the Expression and Updater from a bean property name Allows a custom
250 * updater to be passed in.
251 *
252 * @param elementDescriptor
253 * configure this <code>ElementDescriptor</code>
254 * @param updateMethodName
255 * custom update method. If null, then use standard
256 * @param forceAccessible
257 * if true and updateMethodName is not null, then non-public
258 * methods will be searched and made accessible
259 * (Method.setAccessible(true))
260 */
261 private void configureDescriptor(ElementDescriptor elementDescriptor,
262 String updateMethodName, boolean forceAccessible) {
263 Class beanClass = getBeanClass();
264 if (beanClass != null) {
265 String name = elementDescriptor.getPropertyName();
266 PropertyDescriptor descriptor = getPropertyDescriptor(beanClass,
267 name);
268
269 if (descriptor == null) {
270 if (log.isDebugEnabled()) {
271 log.debug("Cannot find property matching " + name);
272 }
273 } else {
274 configureProperty(elementDescriptor, descriptor,
275 updateMethodName, forceAccessible, beanClass);
276
277 getProcessedPropertyNameSet().add(name);
278 }
279 }
280 }
281
282 /**
283 * Configure an <code>ElementDescriptor</code> from a
284 * <code>PropertyDescriptor</code>. A custom update method may be set.
285 *
286 * @param elementDescriptor
287 * configure this <code>ElementDescriptor</code>
288 * @param propertyDescriptor
289 * configure from this <code>PropertyDescriptor</code>
290 * @param updateMethodName
291 * the name of the custom updater method to user. If null, then
292 * then
293 * @param forceAccessible
294 * if true and updateMethodName is not null, then non-public
295 * methods will be searched and made accessible
296 * (Method.setAccessible(true))
297 * @param beanClass
298 * the <code>Class</code> from which the update method should
299 * be found. This may be null only when
300 * <code>updateMethodName</code> is also null.
301 */
302 private void configureProperty(ElementDescriptor elementDescriptor,
303 PropertyDescriptor propertyDescriptor, String updateMethodName,
304 boolean forceAccessible, Class beanClass) {
305
306 Class type = propertyDescriptor.getPropertyType();
307 Method readMethod = propertyDescriptor.getReadMethod();
308 Method writeMethod = propertyDescriptor.getWriteMethod();
309
310 elementDescriptor.setPropertyType(type);
311
312 // TODO: associate more bean information with the descriptor?
313 // nodeDescriptor.setDisplayName( propertyDescriptor.getDisplayName() );
314 // nodeDescriptor.setShortDescription(
315 // propertyDescriptor.getShortDescription() );
316
317 if (readMethod == null) {
318 log.trace("No read method");
319 return;
320 }
321
322 if (log.isTraceEnabled()) {
323 log.trace("Read method=" + readMethod.getName());
324 }
325
326 // choose response from property type
327
328 final MethodExpression methodExpression = new MethodExpression(readMethod);
329 if (getXMLIntrospector().isPrimitiveType(type)) {
330 elementDescriptor
331 .setTextExpression(methodExpression);
332
333 } else if (getXMLIntrospector().isLoopType(type)) {
334 log.trace("Loop type ??");
335
336 // don't wrap this in an extra element as its specified in the
337 // XML descriptor so no need.
338 Expression expression = methodExpression;
339
340 // Support collectives with standard property setters (not adders)
341 // that use polymorphism to read objects.
342 boolean standardProperty = false;
343 if (updateMethodName != null && writeMethod != null && writeMethod.getName().equals(updateMethodName)) {
344 final Class[] parameters = writeMethod.getParameterTypes();
345 if (parameters.length == 1) {
346 Class setterType = parameters[0];
347 if (type.equals(setterType)) {
348 standardProperty = true;
349 }
350 }
351 }
352 if (!standardProperty) {
353 expression = new IteratorExpression(methodExpression);
354 }
355 elementDescriptor.setContextExpression(expression);
356 elementDescriptor.setHollow(true);
357
358 writeMethod = null;
359
360 if (Map.class.isAssignableFrom(type)) {
361 elementDescriptor.setLocalName("entry");
362 // add elements for reading
363 ElementDescriptor keyDescriptor = new ElementDescriptor("key");
364 keyDescriptor.setHollow(true);
365 elementDescriptor.addElementDescriptor(keyDescriptor);
366
367 ElementDescriptor valueDescriptor = new ElementDescriptor(
368 "value");
369 valueDescriptor.setHollow(true);
370 elementDescriptor.addElementDescriptor(valueDescriptor);
371 }
372
373 } else {
374 log.trace("Standard property");
375 elementDescriptor.setHollow(true);
376 elementDescriptor.setContextExpression(methodExpression);
377 }
378
379 // see if we have a custom method update name
380 if (updateMethodName == null) {
381 // set standard write method
382 if (writeMethod != null) {
383 elementDescriptor.setUpdater(new MethodUpdater(writeMethod));
384 }
385
386 } else {
387 // see if we can find and set the custom method
388 if (log.isTraceEnabled()) {
389 log.trace("Finding custom method: ");
390 log.trace(" on:" + beanClass);
391 log.trace(" name:" + updateMethodName);
392 }
393
394 Method updateMethod;
395 boolean isMapTypeProperty = Map.class.isAssignableFrom(type);
396 if (forceAccessible) {
397 updateMethod = findAnyMethod(updateMethodName, beanClass, isMapTypeProperty);
398 } else {
399 updateMethod = findPublicMethod(updateMethodName, beanClass, isMapTypeProperty);
400 }
401
402 if (updateMethod == null) {
403 if (log.isInfoEnabled()) {
404
405 log.info("No method with name '" + updateMethodName
406 + "' found for update");
407 }
408 } else {
409 // assign updater to elementDescriptor
410 if (Map.class.isAssignableFrom(type)) {
411
412 getXMLIntrospector().assignAdder(updateMethod, elementDescriptor);
413
414 } else {
415 elementDescriptor
416 .setUpdater(new MethodUpdater(updateMethod));
417 Class singularType = updateMethod.getParameterTypes()[0];
418 elementDescriptor.setSingularPropertyType(singularType);
419 if (singularType != null)
420 {
421 boolean isPrimitive = getXMLIntrospector().isPrimitiveType(singularType);
422 if (isPrimitive)
423 {
424 log.debug("Primitive collective: setting hollow to false");
425 elementDescriptor.setHollow(false);
426 }
427 }
428 if (log.isTraceEnabled()) {
429 log.trace("Set custom updater on " + elementDescriptor);
430 }
431 }
432 }
433 }
434 }
435
436 private Method findPublicMethod(String updateMethodName, Class beanType, boolean isMapTypeProperty) {
437 Method[] methods = beanType.getMethods();
438 Method updateMethod = searchMethodsForMatch(updateMethodName, methods, isMapTypeProperty);
439 return updateMethod;
440 }
441
442 private Method searchMethodsForMatch(String updateMethodName,
443 Method[] methods, boolean isMapType) {
444 Method updateMethod = null;
445 for (int i = 0, size = methods.length; i < size; i++) {
446 Method method = methods[i];
447 if (updateMethodName.equals(method.getName())) {
448
449 // updater should have one parameter unless type is Map
450 int numParams = 1;
451 if (isMapType) {
452 // updater for Map should have two parameters
453 numParams = 2;
454 }
455
456 // we have a matching name
457 // check paramters are correct
458 if (methods[i].getParameterTypes().length == numParams) {
459 // we'll use first match
460 updateMethod = methods[i];
461 if (log.isTraceEnabled()) {
462 log.trace("Matched method:" + updateMethod);
463 }
464 // done since we're using the first match
465 break;
466 }
467 }
468 }
469 return updateMethod;
470 }
471
472 private Method findAnyMethod(String updateMethodName, Class beanType, boolean isMapTypeProperty) {
473 // TODO: suspect that this algorithm may run into difficulties
474 // on older JVMs (particularly with package privilage interfaces).
475 // This seems like too esoteric a use case to worry to much about now
476 Method updateMethod = null;
477 Class classToTry = beanType;
478 do {
479 Method[] methods = classToTry.getDeclaredMethods();
480 updateMethod = searchMethodsForMatch(updateMethodName, methods, isMapTypeProperty);
481
482 // try next superclass - Object will return null and end loop if no
483 // method is found
484 classToTry = classToTry.getSuperclass();
485 } while (updateMethod == null && classToTry != null);
486
487 if (updateMethod != null) {
488 boolean isPublic = Modifier.isPublic(updateMethod.getModifiers())
489 && Modifier.isPublic(beanType.getModifiers());
490 if (!isPublic) {
491 updateMethod.setAccessible(true);
492 }
493 }
494 return updateMethod;
495 }
496 }