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 }