001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     * 
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     * 
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */ 
017    
018    package org.apache.commons.betwixt.io;
019    
020    import org.apache.commons.betwixt.BindingConfiguration;
021    import org.apache.commons.betwixt.ElementDescriptor;
022    import org.apache.commons.betwixt.XMLIntrospector;
023    import org.apache.commons.betwixt.expression.Context;
024    import org.apache.commons.betwixt.io.read.BeanBindAction;
025    import org.apache.commons.betwixt.io.read.MappingAction;
026    import org.apache.commons.betwixt.io.read.ReadConfiguration;
027    import org.apache.commons.betwixt.io.read.ReadContext;
028    import org.apache.commons.digester.Digester;
029    import org.apache.commons.digester.Rule;
030    import org.apache.commons.digester.RuleSet;
031    import org.apache.commons.logging.Log;
032    import org.apache.commons.logging.LogFactory;
033    import org.xml.sax.Attributes;
034    
035    /** <p>Sets <code>Betwixt</code> digestion rules for a bean class.</p>
036      *
037      * @author <a href="mailto:rdonkin@apache.org">Robert Burrell Donkin</a>
038      * @author <a href="mailto:martin@mvdb.net">Martin van den Bemt</a>
039      * @since 0.5
040      */
041    public class BeanRuleSet implements RuleSet {
042    
043        /** Logger */
044        private static Log log = LogFactory.getLog(BeanRuleSet.class);
045    
046        /** 
047        * Set log to be used by <code>BeanRuleSet</code> instances 
048        * @param aLog the <code>Log</code> implementation for this class to log to
049        */
050        public static void setLog(Log aLog) {
051            log = aLog;
052        }
053    
054        /** The base path under which the rules will be attached */
055        private String basePath;
056        /** The element descriptor for the base  */
057        private ElementDescriptor baseElementDescriptor;
058        /** The (empty) base context from which all Contexts 
059        with beans are (directly or indirectly) obtained */
060        private DigesterReadContext context;
061        /** allows an attribute to be specified to overload the types of beans used */
062        private String classNameAttribute = "className";
063    
064        /**
065         * Base constructor.
066         *
067         * @param introspector the <code>XMLIntrospector</code> used to introspect 
068         * @param basePath specifies the (Digester-style) path under which the rules will be attached
069         * @param baseElementDescriptor the <code>ElementDescriptor</code> used to create the rules
070         * @param baseBeanClass the <code>Class</code> whose mapping rules will be created
071         * @param matchIDs should ID/IDREFs be used to match beans?
072         * @deprecated 0.5 use constructor which takes a ReadContext instead
073         */
074        public BeanRuleSet(
075            XMLIntrospector introspector,
076            String basePath,
077            ElementDescriptor baseElementDescriptor,
078            Class baseBeanClass,
079            boolean matchIDs) {
080            this.basePath = basePath;
081            this.baseElementDescriptor = baseElementDescriptor;
082            BindingConfiguration bindingConfiguration = new BindingConfiguration();
083            bindingConfiguration.setMapIDs(matchIDs);
084            context =
085                new DigesterReadContext(
086                    log,
087                    bindingConfiguration,
088                    new ReadConfiguration());
089            context.setRootClass(baseBeanClass);
090            context.setXMLIntrospector(introspector);
091        }
092    
093        /**
094         * Base constructor.
095         *
096         * @param introspector the <code>XMLIntrospector</code> used to introspect 
097         * @param basePath specifies the (Digester-style) path under which the rules will be attached
098         * @param baseElementDescriptor the <code>ElementDescriptor</code> used to create the rules
099         * @param context the root Context that bean carrying Contexts should be obtained from, 
100         * not null
101         * @deprecated 0.6 use the constructor which takes a ReadContext instead
102         */
103        public BeanRuleSet(
104            XMLIntrospector introspector,
105            String basePath,
106            ElementDescriptor baseElementDescriptor,
107            Context context) {
108    
109            this.basePath = basePath;
110            this.baseElementDescriptor = baseElementDescriptor;
111            this.context =
112                new DigesterReadContext(context, new ReadConfiguration());
113            this.context.setRootClass(
114                baseElementDescriptor.getSingularPropertyType());
115            this.context.setXMLIntrospector(introspector);
116        }
117    
118        /**
119         * Base constructor.
120         *
121         * @param introspector the <code>XMLIntrospector</code> used to introspect 
122         * @param basePath specifies the (Digester-style) path under which the rules will be attached
123         * @param baseElementDescriptor the <code>ElementDescriptor</code> used to create the rules
124         * @param baseBeanClass the <code>Class</code> whose mapping rules will be created
125         * @param context the root Context that bean carrying Contexts should be obtained from, 
126         * not null
127         * @deprecated 0.5 use the constructor which takes a ReadContext instead
128         */
129        public BeanRuleSet(
130                            XMLIntrospector introspector,
131                            String basePath, 
132                            ElementDescriptor baseElementDescriptor, 
133                            Class baseBeanClass,
134                            Context context) {
135            this(
136                introspector,
137                basePath,
138                baseElementDescriptor,
139                baseBeanClass,
140                new ReadContext( context, new ReadConfiguration() ));
141        }
142    
143        /**
144         * Base constructor.
145         *
146         * @param introspector the <code>XMLIntrospector</code> used to introspect 
147         * @param basePath specifies the (Digester-style) path under which the rules will be attached
148         * @param baseElementDescriptor the <code>ElementDescriptor</code> used to create the rules
149         * @param baseBeanClass the <code>Class</code> whose mapping rules will be created
150         * @param baseContext the root Context that bean carrying Contexts should be obtained from, 
151         * not null
152         */
153        public BeanRuleSet(
154            XMLIntrospector introspector,
155            String basePath,
156            ElementDescriptor baseElementDescriptor,
157            Class baseBeanClass,
158            ReadContext baseContext) {
159            this.basePath = basePath;
160            this.baseElementDescriptor = baseElementDescriptor;
161            this.context = new DigesterReadContext(baseContext);
162            this.context.setRootClass(baseBeanClass);
163            this.context.setXMLIntrospector(introspector);
164        }
165    
166        /**
167         * The name of the attribute which can be specified in the XML to override the
168         * type of a bean used at a certain point in the schema.
169         *
170         * <p>The default value is 'className'.</p>
171         * 
172         * @return The name of the attribute used to overload the class name of a bean
173         */
174        public String getClassNameAttribute() {
175            return context.getClassNameAttribute();
176        }
177    
178        /**
179         * Sets the name of the attribute which can be specified in 
180         * the XML to override the type of a bean used at a certain 
181         * point in the schema.
182         *
183         * <p>The default value is 'className'.</p>
184         * 
185         * @param classNameAttribute The name of the attribute used to overload the class name of a bean
186         * @deprecated 0.5 set the <code>ReadContext</code> property instead
187         */
188        public void setClassNameAttribute(String classNameAttribute) {
189            context.setClassNameAttribute(classNameAttribute);
190        }
191    
192        //-------------------------------- Ruleset implementation
193    
194        /** 
195         * <p>Gets the namespace associated with this ruleset.</p>
196         *
197         * <p><strong>Note</strong> namespaces are not currently supported.</p>
198         * 
199         * @return null
200         */
201        public String getNamespaceURI() {
202            return null;
203        }
204    
205        /**
206         * Add rules for bean to given <code>Digester</code>.
207         *
208         * @param digester the <code>Digester</code> to which the rules for the bean will be added
209         */
210        public void addRuleInstances(Digester digester) {
211            if (log.isTraceEnabled()) {
212                log.trace("Adding rules to:" + digester);
213            }
214    
215            context.setDigester(digester);
216    
217            // if the classloader is not set, set to the digester classloader
218            if (context.getClassLoader() == null) {
219                context.setClassLoader(digester.getClassLoader());
220            }
221    
222            // TODO: need to think about strategy for paths
223            // may need to provide a default path and then allow the user to override
224            digester.addRule("!" + basePath + "/*", new ActionMappingRule());
225        }
226    
227        /**
228         * Single rule that is used to map all elements.
229         * 
230         * @author <a href='http://commons.apache.org/'>Apache Commons Team</a>
231         */
232        private final class ActionMappingRule extends Rule {
233    
234            /**
235              * Processes the start of a new <code>Element</code>.
236              * The actual processing is delegated to <code>MappingAction</code>'s.
237              * @see Rule#begin(String, String, Attributes)
238              */
239            public void begin(String namespace, String name, Attributes attributes)
240                throws Exception {
241    
242                if (log.isTraceEnabled()) {
243                    int attributesLength = attributes.getLength();
244                    if (attributesLength > 0) {
245                        log.trace("Attributes:");
246                    }
247                    for (int i = 0, size = attributesLength; i < size; i++) {
248                        log.trace("Local:" + attributes.getLocalName(i));
249                        log.trace("URI:" + attributes.getURI(i));
250                        log.trace("QName:" + attributes.getQName(i));
251                    }
252                }
253    
254                context.pushElement(name);
255                
256                MappingAction nextAction =
257                    nextAction(namespace, name, attributes, context);
258    
259                context.pushMappingAction(nextAction);
260            }
261    
262            /**
263             * Gets the next action to be executed 
264             * @param namespace the element's namespace, not null
265             * @param name the element name, not null
266             * @param attributes the element's attributes, not null
267             * @param context the <code>ReadContext</code> against which the xml is being mapped.
268             * @return the initialized <code>MappingAction</code>, not null
269             * @throws Exception
270             */
271            private MappingAction nextAction(
272                String namespace,
273                String name,
274                Attributes attributes,
275                ReadContext context)
276                throws Exception {
277                    
278                MappingAction result = null;
279                MappingAction lastAction = context.currentMappingAction();
280                if (lastAction == null)
281                {
282                    result =  BeanBindAction.INSTANCE;   
283                } else {
284                    
285                    result = lastAction.next(namespace, name, attributes, context);
286                }
287                return result.begin(namespace, name, attributes, context);
288            }
289    
290    
291    
292            /**
293            * Processes the body text for the current element.
294            * This is delegated to the current <code>MappingAction</code>.
295            * @see Rule#body(String, String, String)
296            */
297            public void body(String namespace, String name, String text)
298                throws Exception {
299    
300                if (log.isTraceEnabled()) log.trace("[BRS] Body with text " + text);
301                if (digester.getCount() > 0) {
302                    MappingAction action = context.currentMappingAction();
303                    action.body(text, context);
304                } else {
305                    log.trace("[BRS] ZERO COUNT");
306                }
307            }
308    
309            /**
310            * Process the end of this element.
311            * This is delegated to the current <code>MappingAction</code>.
312            */
313            public void end(String namespace, String name) throws Exception {
314    
315                MappingAction action = context.popMappingAction();
316                action.end(context);
317            }
318    
319            /** 
320             * Tidy up.
321             */
322            public void finish() {
323                //
324                // Clear indexed beans so that we're ready to process next document
325                //
326                context.clearBeans();
327            }
328    
329        }
330    
331        /**
332         * Specialization of <code>ReadContext</code> when reading from <code>Digester</code>.
333         * @author <a href='http://commons.apache.org/'>Apache Commons Team</a>
334         * @version $Revision: 561314 $
335         */
336        private static class DigesterReadContext extends ReadContext {
337    
338            private Digester digester;
339    
340            /**
341             * @param context
342             * @param readConfiguration
343             */
344            public DigesterReadContext(
345                Context context,
346                ReadConfiguration readConfiguration) {
347                super(context, readConfiguration);
348                // TODO Auto-generated constructor stub
349            }
350    
351            /**
352             * @param bindingConfiguration
353             * @param readConfiguration
354             */
355            public DigesterReadContext(
356                BindingConfiguration bindingConfiguration,
357                ReadConfiguration readConfiguration) {
358                super(bindingConfiguration, readConfiguration);
359            }
360    
361            /**
362             * @param log
363             * @param bindingConfiguration
364             * @param readConfiguration
365             */
366            public DigesterReadContext(
367                Log log,
368                BindingConfiguration bindingConfiguration,
369                ReadConfiguration readConfiguration) {
370                super(log, bindingConfiguration, readConfiguration);
371            }
372    
373            /**
374             * @param log
375             * @param bindingConfiguration
376             * @param readConfiguration
377             */
378            public DigesterReadContext(ReadContext readContext) {
379                super(readContext);
380            }
381    
382            public Digester getDigester() {
383                // TODO: replace with something better
384                return digester;
385            }
386    
387            public void setDigester(Digester digester) {
388                // TODO: replace once moved to single Rule
389                this.digester = digester;
390            }
391    
392            /* (non-Javadoc)
393             * @see org.apache.commons.betwixt.io.read.ReadContext#pushBean(java.lang.Object)
394             */
395            public void pushBean(Object bean) {
396                super.pushBean(bean);
397                digester.push(bean);
398            }
399    
400            /* (non-Javadoc)
401             * @see org.apache.commons.betwixt.io.read.ReadContext#putBean(java.lang.Object)
402             */
403            public Object popBean() {
404                Object bean = super.popBean();
405                // don't pop the last from the stack
406                if (digester.getCount() > 0) {
407                    digester.pop();
408                }
409                return bean;
410            }
411        }
412    
413    }