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