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    }