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 }