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 <bt:manager> 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 }