001 /* 002 * JBoss, Home of Professional Open Source. 003 * Copyright 2008, Red Hat Middleware LLC, and individual contributors 004 * as indicated by the @author tags. See the copyright.txt file in the 005 * distribution for a full listing of individual contributors. 006 * 007 * This is free software; you can redistribute it and/or modify it 008 * under the terms of the GNU Lesser General Public License as 009 * published by the Free Software Foundation; either version 2.1 of 010 * the License, or (at your option) any later version. 011 * 012 * This software is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * You should have received a copy of the GNU Lesser General Public 018 * License along with this software; if not, write to the Free 019 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 020 * 02110-1301 USA, or see the FSF site: http://www.fsf.org. 021 */ 022 package org.jboss.dna.repository.rules; 023 024 import java.util.HashMap; 025 import java.util.HashSet; 026 import java.util.Map; 027 import java.util.Set; 028 import java.util.concurrent.ExecutorService; 029 import java.util.concurrent.Executors; 030 import java.util.regex.Matcher; 031 import java.util.regex.Pattern; 032 import java.util.regex.PatternSyntaxException; 033 import javax.jcr.Node; 034 import javax.jcr.RepositoryException; 035 import javax.jcr.Session; 036 import net.jcip.annotations.ThreadSafe; 037 import org.jboss.dna.common.collection.SimpleProblems; 038 import org.jboss.dna.common.util.CheckArg; 039 import org.jboss.dna.common.util.Logger; 040 import org.jboss.dna.repository.RepositoryI18n; 041 import org.jboss.dna.repository.observation.NodeChange; 042 import org.jboss.dna.repository.observation.NodeChangeListener; 043 import org.jboss.dna.repository.observation.NodeChanges; 044 import org.jboss.dna.repository.observation.ObservationService; 045 import org.jboss.dna.repository.util.JcrExecutionContext; 046 import org.jboss.dna.repository.util.JcrTools; 047 048 /** 049 * A component that can listen to a JCR repository and keep the {@link RuleSet} instances of a {@link RuleService} synchronized 050 * with that repository. 051 * <p> 052 * This class is a {@link NodeChangeListener} that can {@link ObservationService#addListener(NodeChangeListener) subscribe} to 053 * changes in one or more JCR repositories being monitored by an {@link ObservationService}. As changes under the rule sets 054 * branch are discovered, they are processed asynchronously. This ensure that the processing of the repository contents does not 055 * block the other listeners of the {@link ObservationService}. 056 * </p> 057 * 058 * @author Randall Hauch 059 */ 060 @ThreadSafe 061 public class RuleSetRepositoryMonitor implements NodeChangeListener { 062 063 public static final String DEFAULT_JCR_ABSOLUTE_PATH = "/dna:system/dna:ruleSets/"; 064 065 protected static final String JCR_PATH_DELIM = "/"; 066 067 private final JcrExecutionContext executionContext; 068 private final RuleService ruleService; 069 private final String jcrAbsolutePath; 070 private final Pattern ruleSetNamePattern; 071 private final ExecutorService executorService; 072 private Logger logger; 073 074 /** 075 * Create an instance that can listen to the {@link RuleSet} definitions stored in a JCR repository and ensure that the 076 * {@link RuleSet} instances of a {@link RuleService} reflect the definitions in the repository. 077 * 078 * @param ruleService the rule service that should be kept in sync with the JCR repository. 079 * @param jcrAbsolutePath the absolute path to the branch where the rule sets are defined; if null or empty, the 080 * {@link #DEFAULT_JCR_ABSOLUTE_PATH default path} is used 081 * @param executionContext the context in which this monitor is to execute 082 * @throws IllegalArgumentException if the rule service or execution context is null, or if the supplied 083 * <code>jcrAbsolutePath</code> is invalid 084 */ 085 public RuleSetRepositoryMonitor( RuleService ruleService, 086 String jcrAbsolutePath, 087 JcrExecutionContext executionContext ) { 088 CheckArg.isNotNull(ruleService, "rule service"); 089 CheckArg.isNotNull(executionContext, "execution context"); 090 this.ruleService = ruleService; 091 this.executionContext = executionContext; 092 this.executorService = Executors.newSingleThreadExecutor(); 093 this.logger = Logger.getLogger(this.getClass()); 094 if (jcrAbsolutePath != null) jcrAbsolutePath = jcrAbsolutePath.trim(); 095 this.jcrAbsolutePath = jcrAbsolutePath != null && jcrAbsolutePath.length() != 0 ? jcrAbsolutePath : DEFAULT_JCR_ABSOLUTE_PATH; 096 try { 097 // Create the pattern to extract the rule set name from the absolute path ... 098 String leadingPath = this.jcrAbsolutePath; 099 if (!leadingPath.endsWith(JCR_PATH_DELIM)) leadingPath = leadingPath + JCR_PATH_DELIM; 100 this.ruleSetNamePattern = Pattern.compile(leadingPath + "([^/]+)/?.*"); 101 } catch (PatternSyntaxException e) { 102 throw new IllegalArgumentException( 103 RepositoryI18n.unableToBuildRuleSetRegularExpressionPattern.text(e.getPattern(), 104 jcrAbsolutePath, 105 e.getDescription())); 106 } 107 } 108 109 /** 110 * @return ruleService 111 */ 112 public RuleService getRuleService() { 113 return this.ruleService; 114 } 115 116 /** 117 * @return jcrAbsolutePath 118 */ 119 public String getAbsolutePathToRuleSets() { 120 return this.jcrAbsolutePath; 121 } 122 123 /** 124 * @return logger 125 */ 126 public Logger getLogger() { 127 return this.logger; 128 } 129 130 /** 131 * @param logger Sets logger to the specified value. 132 */ 133 public void setLogger( Logger logger ) { 134 this.logger = logger; 135 } 136 137 /** 138 * {@inheritDoc} 139 */ 140 public void onNodeChanges( NodeChanges changes ) { 141 final Map<String, Set<String>> ruleSetNamesByWorkspaceName = new HashMap<String, Set<String>>(); 142 for (NodeChange nodeChange : changes) { 143 if (nodeChange.isNotOnPath(this.jcrAbsolutePath)) continue; 144 // Use a regular expression on the absolute path to get the name of the rule set that is affected ... 145 Matcher matcher = this.ruleSetNamePattern.matcher(nodeChange.getAbsolutePath()); 146 if (!matcher.matches()) continue; 147 String ruleSetName = matcher.group(1); 148 // Record the repository name ... 149 String workspaceName = nodeChange.getRepositoryWorkspaceName(); 150 Set<String> ruleSetNames = ruleSetNamesByWorkspaceName.get(workspaceName); 151 if (ruleSetNames == null) { 152 ruleSetNames = new HashSet<String>(); 153 ruleSetNamesByWorkspaceName.put(workspaceName, ruleSetNames); 154 } 155 // Record the rule set name ... 156 ruleSetNames.add(ruleSetName); 157 158 } 159 if (ruleSetNamesByWorkspaceName.isEmpty()) return; 160 // Otherwise there are changes, so submit the names to the executor service ... 161 this.executorService.execute(new Runnable() { 162 163 public void run() { 164 processRuleSets(ruleSetNamesByWorkspaceName); 165 } 166 }); 167 } 168 169 /** 170 * Process the rule sets given by the supplied names, keyed by the repository workspace name. 171 * 172 * @param ruleSetNamesByWorkspaceName the set of rule set names keyed by the repository workspace name 173 */ 174 protected void processRuleSets( Map<String, Set<String>> ruleSetNamesByWorkspaceName ) { 175 final JcrTools tools = this.executionContext.getTools(); 176 final String relPathToRuleSets = getAbsolutePathToRuleSets().substring(1); 177 for (Map.Entry<String, Set<String>> entry : ruleSetNamesByWorkspaceName.entrySet()) { 178 String workspaceName = entry.getKey(); 179 Session session = null; 180 try { 181 session = this.executionContext.getSessionFactory().createSession(workspaceName); 182 // Look up the node that represents the parent of the rule set nodes ... 183 Node ruleSetsNode = session.getRootNode().getNode(relPathToRuleSets); 184 185 for (String ruleSetName : entry.getValue()) { 186 // Look up the node that represents the rule set... 187 if (ruleSetsNode.hasNode(ruleSetName)) { 188 // We don't handle multiple siblings with the same name, so this should grab the first one ... 189 Node ruleSetNode = ruleSetsNode.getNode(ruleSetName); 190 RuleSet ruleSet = buildRuleSet(ruleSetName, ruleSetNode, tools); 191 if (ruleSet != null) { 192 // Only do something if the RuleSet was instantiated ... 193 getRuleService().addRuleSet(ruleSet); 194 } 195 } else { 196 // The node doesn't exist, so remove the rule set ... 197 getRuleService().removeRuleSet(ruleSetName); 198 } 199 } 200 } catch (RepositoryException e) { 201 getLogger().error(e, RepositoryI18n.errorObtainingSessionToRepositoryWorkspace, workspaceName); 202 } finally { 203 if (session != null) session.logout(); 204 } 205 } 206 } 207 208 /** 209 * Create a rule set from the supplied node. This is called whenever a branch of the repository is changed. 210 * <p> 211 * This implementation expects a node of type 'dna:ruleSet' and the following properties (expressed as XPath statements 212 * relative to the supplied node): 213 * <ul> 214 * <li>The {@link RuleSet#getDescription() description} is obtained from the "<code>./@jcr:description</code>" string 215 * property. This property is optional.</li> 216 * <li>The {@link RuleSet#getComponentClassname() classname} is obtained from the "<code>./@dna:classname</code>" string 217 * property. This property is required.</li> 218 * <li>The {@link RuleSet#getComponentClasspath() classpath} is obtained from the "<code>./@dna:classpath</code>" string 219 * property. This property is optional, and if abscent then the classpath will be assumed from the current context.</li> 220 * <li>The {@link RuleSet#getProviderUri() provider URI} is obtained from the "<code>./@dna:serviceProviderUri</code>" 221 * string property, and corresponds to the URI of the JSR-94 rules engine service provider. This property is required.</li> 222 * <li>The {@link RuleSet#getRuleSetUri() rule set URI} is obtained from the "<code>./@dna:ruleSetUri</code>" string 223 * property. This property is optional and defaults to the node name (e.g., "<code>./@jcr:name</code>").</li> 224 * <li>The {@link RuleSet#getRules() definition of the rules} is obtained from the "<code>./@dna:rules</code>" string 225 * property. This property is required and must be in a form suitable for the JSR-94 rules engine.</li> 226 * <li>The {@link RuleSet#getProperties() properties} are obtained from the "<code>./dna:properties[contains(@jcr:mixinTypes,'dna:propertyContainer')]/*[@jcr:nodeType='dna:property']</code>" 227 * property nodes, where the name of the property is extracted from the property node's "<code>./@jcr:name</code>" string 228 * property and the value of the property is extracted from the property node's "<code>./@dna:propertyValue</code>" string 229 * property. Rule set properties are optional.</li> 230 * </ul> 231 * </p> 232 * 233 * @param name the name of the rule set; never null 234 * @param ruleSetNode the node representing the rule set; null if the rule set doesn't exist 235 * @param tools 236 * @return the rule set for the information stored in the repository, or null if the rule set does not exist or has errors 237 */ 238 protected RuleSet buildRuleSet( String name, 239 Node ruleSetNode, 240 JcrTools tools ) { 241 if (ruleSetNode == null) return null; 242 243 SimpleProblems simpleProblems = new SimpleProblems(); 244 String description = tools.getPropertyAsString(ruleSetNode, "jcr:description", false, simpleProblems); 245 String classname = tools.getPropertyAsString(ruleSetNode, "dna:classname", true, simpleProblems); 246 String[] classpath = tools.getPropertyAsStringArray(ruleSetNode, "dna:classpath", false, simpleProblems); 247 String providerUri = tools.getPropertyAsString(ruleSetNode, "dna:serviceProviderUri", true, simpleProblems); 248 String ruleSetUri = tools.getPropertyAsString(ruleSetNode, "dna:ruleSetUri", true, name, simpleProblems); 249 String rules = tools.getPropertyAsString(ruleSetNode, "dna:rules", true, simpleProblems); 250 Map<String, Object> properties = tools.loadProperties(ruleSetNode, simpleProblems); 251 if (simpleProblems.hasProblems()) { 252 // There are problems, so store and save them, and then return null ... 253 try { 254 if (tools.storeProblems(ruleSetNode, simpleProblems)) ruleSetNode.save(); 255 } catch (RepositoryException e) { 256 this.logger.error(e, RepositoryI18n.errorWritingProblemsOnRuleSet, tools.getReadable(ruleSetNode)); 257 } 258 return null; 259 } 260 // There are no problems with this rule set, so make sure that there are no persisted problems anymore ... 261 try { 262 if (tools.removeProblems(ruleSetNode)) ruleSetNode.save(); 263 } catch (RepositoryException e) { 264 this.logger.error(e, RepositoryI18n.errorWritingProblemsOnRuleSet, tools.getReadable(ruleSetNode)); 265 } 266 return new RuleSet(name, description, classname, classpath, providerUri, ruleSetUri, rules, properties); 267 } 268 269 }