001    /* 
002     * Copyright 2008-2009 the original author or authors.
003     * The contents of this file are subject to the Mozilla Public License
004     * Version 1.1 (the "License"); you may not use this file except in
005     * compliance with the License. You may obtain a copy of the License at
006     * http://www.mozilla.org/MPL/
007     *
008     * Software distributed under the License is distributed on an "AS IS"
009     * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
010     * License for the specific language governing rights and limitations
011     * under the License.
012     */
013     
014    package com.mtgi.analytics.aop.config.v11;
015    
016    import static com.mtgi.analytics.aop.config.v11.BtXmlPersisterBeanDefinitionParser.CONFIG_PERSISTER;
017    import static com.mtgi.analytics.aop.config.v11.ConfigurationConstants.CONFIG_EXECUTOR;
018    import static com.mtgi.analytics.aop.config.v11.ConfigurationConstants.CONFIG_MANAGER;
019    import static com.mtgi.analytics.aop.config.v11.ConfigurationConstants.CONFIG_NAMESPACE;
020    import static com.mtgi.analytics.aop.config.v11.ConfigurationConstants.CONFIG_SCHEDULER;
021    import static com.mtgi.analytics.aop.config.v11.ConfigurationConstants.CONFIG_SESSION_CONTEXT;
022    import static com.mtgi.analytics.aop.config.v11.ConfigurationConstants.CONFIG_TEMPLATE;
023    
024    import java.util.HashSet;
025    import java.util.Set;
026    
027    import org.springframework.aop.aspectj.AspectJExpressionPointcut;
028    import org.springframework.aop.config.AopNamespaceUtils;
029    import org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor;
030    import org.springframework.beans.MutablePropertyValues;
031    import org.springframework.beans.factory.config.BeanDefinition;
032    import org.springframework.beans.factory.config.BeanDefinitionHolder;
033    import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
034    import org.springframework.beans.factory.config.RuntimeBeanNameReference;
035    import org.springframework.beans.factory.config.RuntimeBeanReference;
036    import org.springframework.beans.factory.parsing.CompositeComponentDefinition;
037    import org.springframework.beans.factory.support.DefaultListableBeanFactory;
038    import org.springframework.beans.factory.support.RootBeanDefinition;
039    import org.springframework.beans.factory.xml.NamespaceHandler;
040    import org.springframework.beans.factory.xml.ParserContext;
041    import org.springframework.beans.factory.xml.XmlReaderContext;
042    import org.springframework.web.context.WebApplicationContext;
043    import org.w3c.dom.Element;
044    import org.w3c.dom.Node;
045    import org.w3c.dom.NodeList;
046    
047    import com.mtgi.analytics.BehaviorEventPersister;
048    import com.mtgi.analytics.BehaviorTrackingManagerImpl;
049    import com.mtgi.analytics.SessionContext;
050    import com.mtgi.analytics.XmlBehaviorEventPersisterImpl;
051    import com.mtgi.analytics.aop.BehaviorTrackingAdvice;
052    import com.mtgi.analytics.aop.config.TemplateBeanDefinitionParser;
053    import com.mtgi.analytics.servlet.SpringSessionContext;
054    
055    /**
056     * Parser for <bt:manager> configuration tags, the most significant tag in behavior tracking configuration.
057     * Each such tag will configure an instance of {@link BehaviorTrackingManagerImpl} and bind it into the Spring application
058     * context.  This includes configuring an implementation of {@link BehaviorEventPersister}, {@link SessionContext},
059     * registering AOP advice for tracking method calls, and automatically scheduling event flushes and log rotation
060     * according to sensible default values.
061     */
062    public class BtManagerBeanDefinitionParser extends TemplateBeanDefinitionParser  {
063    
064            /** The bean name of the bt manager instance, defaults to <code>defaultTrackingManager</code>. */
065            public static final String ATT_ID = "id";
066            /** 
067             * The name of the application, required to be specified at runtime.  
068             * @see BehaviorTrackingManagerImpl#setApplication(String) 
069             */
070            public static final String ATT_APPLICATION = "application";
071            /** 
072             * Bean name reference to a TaskExecutor for executing event flushes and other scheduled operations.  
073             * A private instance is created if one is not specified.
074             * @see BehaviorTrackingManagerImpl#setExecutor(org.springframework.core.task.TaskExecutor)
075             */
076            public static final String ATT_TASK_EXECUTOR = "task-executor";
077            /** @see BehaviorTrackingManagerImpl#setFlushThreshold(int)  */
078            public static final String ATT_FLUSH_THRESHOLD = "flush-threshold";
079            /** 
080             * Bean name reference to a Quartz Scheduler used for scheduled operations like event flush and log rotation.
081             * A private instance is created if one is not specified.
082             */
083            public static final String ATT_SCHEDULER = "scheduler";
084            /**
085             * Quartz Cron Expression identifying how often behavior events are flushed to the persister.  Defaults
086             * to once every 5 minutes if unspecified.
087             */
088            public static final String ATT_FLUSH_SCHEDULE = "flush-schedule";
089            /**
090             * AOP method pattern identifying which methods should be logged as behavior tracking events.  Defaults to
091             * null if unspecified.
092             */
093            public static final String ATT_METHOD_EXPRESSION = "track-method-expression";
094            /**
095             * Bean name reference to an implementation of {@link BehaviorEventPersister} defined in the application context.
096             * A private instance of {@link XmlBehaviorEventPersisterImpl} is created if none is specified.  This attribute
097             * cannot be used in combination with a nested persister tag (<code>xml-persister</code>, <code>jdbc-persister</code>,
098             * <code>custom-persister</code>).
099             * @see BehaviorTrackingManagerImpl#setPersister(BehaviorEventPersister)
100             */
101            public static final String ATT_PERSISTER = "persister";
102            /**
103             * Bean name reference to an implementation of {@link SessionContext} defined in the application context.
104             * A private instance is created if none is specified.  This attribute
105             * cannot be used in combination with a nested bt:session-context tag.
106             * @see BehaviorTrackingManagerImpl#setSessionContext(SessionContext)
107             */
108            public static final String ATT_SESSION_CONTEXT = "session-context";
109    
110            public BtManagerBeanDefinitionParser() {
111                    super(CONFIG_TEMPLATE, CONFIG_MANAGER);
112            }
113    
114            @Override
115            protected void transform(ConfigurableListableBeanFactory factory,
116                            BeanDefinition template, Element element,
117                            ParserContext parserContext) {
118    
119                    ManagerComponentDefinition def = (ManagerComponentDefinition)parserContext.getContainingComponent();
120                    
121                    String managerId = overrideAttribute(ATT_ID, template, element);
122                    if (managerId == null)
123                            template.setAttribute(ATT_ID, managerId = "defaultTrackingManager");
124    
125                    overrideProperty(ATT_APPLICATION, template, element, false);
126                    overrideProperty(ATT_FLUSH_THRESHOLD, template, element, false);
127    
128                    if (element.hasAttribute(ATT_SCHEDULER))
129                            factory.registerAlias(element.getAttribute(ATT_SCHEDULER), CONFIG_SCHEDULER);
130                    
131                    if (element.hasAttribute(ATT_TASK_EXECUTOR))
132                            factory.registerAlias(element.getAttribute(ATT_TASK_EXECUTOR), CONFIG_EXECUTOR);
133                    
134                    if (element.hasAttribute(ATT_PERSISTER)) {
135                            //override default persister definition with reference
136                            def.addNestedProperty(ATT_PERSISTER);
137                            factory.registerAlias(element.getAttribute(ATT_PERSISTER), CONFIG_PERSISTER);
138                    }
139                    
140                    if (element.hasAttribute(ATT_SESSION_CONTEXT)) {
141                            //override default session context with reference
142                            def.addNestedProperty("sessionContext");
143                            factory.registerAlias(element.getAttribute(ATT_SESSION_CONTEXT), CONFIG_SESSION_CONTEXT);
144                    }
145    
146                    //handle AOP configuration if needed
147                    if (element.hasAttribute(ATT_METHOD_EXPRESSION)) {
148                            //activate global AOP proxying if it hasn't already been done (borrowed logic from AopNamespaceHandler / config element parser)
149                            AopNamespaceUtils.registerAspectJAutoProxyCreatorIfNecessary(parserContext, element);
150                            
151                            //register pointcut definition for the provided expression.
152                            RootBeanDefinition pointcut = new RootBeanDefinition(AspectJExpressionPointcut.class);
153                            pointcut.setSingleton(false);
154                            pointcut.setSynthetic(true);
155                            pointcut.getPropertyValues().addPropertyValue("expression", element.getAttribute(ATT_METHOD_EXPRESSION));
156    
157                            //create implicit pointcut advice bean.
158                            RootBeanDefinition advice = new RootBeanDefinition(BehaviorTrackingAdvice.class);
159                            overrideProperty(ATT_APPLICATION, advice, element, false);
160                            advice.getPropertyValues().addPropertyValue("trackingManager", new RuntimeBeanReference(managerId));
161    
162                            //register advice, pointcut, and advisor entry to bind the two together.
163                            XmlReaderContext ctx = parserContext.getReaderContext();
164                            String pointcutId = ctx.registerWithGeneratedName(pointcut);
165                            String adviceId = ctx.registerWithGeneratedName(advice);
166                            
167                            RootBeanDefinition advisorDefinition = new RootBeanDefinition(DefaultBeanFactoryPointcutAdvisor.class);
168                            advisorDefinition.getPropertyValues().addPropertyValue("adviceBeanName", new RuntimeBeanNameReference(adviceId));
169                            advisorDefinition.getPropertyValues().addPropertyValue("pointcut", new RuntimeBeanReference(pointcutId));
170                            ctx.registerWithGeneratedName(advisorDefinition);
171                    }
172    
173                    //configure flush trigger and job to be globally unique based on manager name.
174                    BeanDefinition flushTrigger = factory.getBeanDefinition("com.mtgi.analytics.btFlushTrigger");
175                    SchedulerActivationPostProcessor.configureTriggerDefinition(flushTrigger, element.getAttribute(ATT_FLUSH_SCHEDULE), managerId + "_flush");
176    
177                    //set up a post-processor to register the flush job with the selected scheduler instance.  the job and scheduler
178                    //come from the template factory, but the post-processor runs when the currently-parsing factory is finished.
179                    SchedulerActivationPostProcessor.registerPostProcessor(parserContext, factory, CONFIG_SCHEDULER, CONFIG_NAMESPACE + ".btFlushTrigger");
180                    
181                    //ManagerComponentDefinition is a flag to nested parsers that they should push their parsed bean definitions into
182                    //the manager bean definition.  for example, see BtPersisterBeanDefinitionParser.
183                    //descend on nested child nodes to pick up persister and session context configuration
184                    NodeList children = element.getChildNodes();
185                    for (int i = 0; i < children.getLength(); i++) {
186                            Node node = children.item(i);
187                            if (node.getNodeType() == Node.ELEMENT_NODE) {
188                                    String namespaceUri = node.getNamespaceURI();
189                                    NamespaceHandler handler = parserContext.getReaderContext().getNamespaceHandlerResolver().resolve(namespaceUri);
190                                    ParserContext nestedCtx = new ParserContext(parserContext.getReaderContext(), parserContext.getDelegate(), template);
191                                    nestedCtx.pushContainingComponent(def);
192                                    handler.parse((Element)node, nestedCtx);
193                            }
194                    }
195                    
196                    if (!def.nestedProperties.contains(ATT_PERSISTER)) {
197                            //custom persister not registered.  schedule default log rotation trigger.
198                            BtXmlPersisterBeanDefinitionParser.configureLogRotation(parserContext, factory, null);
199                    }
200                    
201                    if (!def.nestedProperties.contains("sessionContext")) {
202                            //custom session context not registered.  select appropriate default class
203                            //depending on whether we are in a web context or not.
204                            if (parserContext.getReaderContext().getReader().getResourceLoader() instanceof WebApplicationContext) {
205                                    BeanDefinition scDef = factory.getBeanDefinition(CONFIG_SESSION_CONTEXT);
206                                    scDef.setBeanClassName(SpringSessionContext.class.getName());
207                            }
208                    }
209            }
210            
211            /** overridden to return an instance of {@link ManagerComponentDefinition} */
212            @Override
213            protected TemplateComponentDefinition newComponentDefinition(String name,
214                            Object source, DefaultListableBeanFactory factory) {
215                    return new ManagerComponentDefinition(name, source, factory);
216            }
217            
218            /** 
219             * called by nested tags to push inner beans into the enclosing {@link BehaviorTrackingManagerImpl}.
220             * @return <span>true if the inner bean was added to an enclosing {@link BehaviorTrackingManagerImpl}.  Otherwise, the bean definition
221             * is not nested inside a &lt;bt:manager&gt; tag and therefore will have to be registered as a global bean in the application
222             * context.</span>
223             */
224            protected static boolean registerNestedBean(BeanDefinitionHolder nested, String parentProperty, ParserContext parserContext) {
225                    //add parsed inner bean element to containing manager definition; e.g. persister or SessionContext impls.
226                    CompositeComponentDefinition parent = parserContext.getContainingComponent();
227                    if (parent instanceof ManagerComponentDefinition) {
228                            //we are nested; add to enclosing bean def.
229                            BeanDefinition managerDef = parserContext.getContainingBeanDefinition();
230    
231                            MutablePropertyValues props = managerDef.getPropertyValues();
232                            props.removePropertyValue(parentProperty);
233                            props.addPropertyValue(parentProperty, nested);
234                            
235                            ((ManagerComponentDefinition)parent).addNestedProperty(parentProperty);
236                            return true;
237                    }
238                    //bean is not nested inside bt:manager
239                    return false;
240            }
241    
242            /** 
243             * Specialized {@link TemplateComponentDefinition} for the <code>bt:manager</code> config tag.
244             * Adds some extra validation to make sure that duplicate definitions are not given for dependencies
245             * (for example, specifying a persister both as a nested element and as a reference attribute).
246             */
247            public static class ManagerComponentDefinition extends TemplateComponentDefinition {
248    
249                    protected ManagerComponentDefinition(String name, Object source,
250                                    DefaultListableBeanFactory factory) {
251                            super(name, source, factory);
252                    }
253    
254                    private Set<String> nestedProperties = new HashSet<String>();
255    
256                    public void addNestedProperty(String property) {
257                            if (!nestedProperties.add(property))
258                                    throw new IllegalArgumentException("Property " + property + " specified more than once");
259                    }
260    
261            }
262    
263    }