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    }