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.sequencers;
023    
024    import java.security.AccessControlContext;
025    import java.util.ArrayList;
026    import java.util.HashMap;
027    import java.util.HashSet;
028    import java.util.List;
029    import java.util.Map;
030    import java.util.Set;
031    import java.util.concurrent.ExecutorService;
032    import java.util.concurrent.Executors;
033    import java.util.concurrent.RejectedExecutionException;
034    import java.util.concurrent.TimeUnit;
035    import java.util.concurrent.atomic.AtomicBoolean;
036    import java.util.concurrent.atomic.AtomicLong;
037    import javax.jcr.Node;
038    import javax.jcr.Repository;
039    import javax.jcr.RepositoryException;
040    import javax.jcr.Session;
041    import javax.jcr.observation.Event;
042    import javax.security.auth.Subject;
043    import javax.security.auth.login.LoginContext;
044    import net.jcip.annotations.Immutable;
045    import net.jcip.annotations.ThreadSafe;
046    import org.jboss.dna.common.collection.SimpleProblems;
047    import org.jboss.dna.common.component.ClassLoaderFactory;
048    import org.jboss.dna.common.component.ComponentLibrary;
049    import org.jboss.dna.common.component.StandardClassLoaderFactory;
050    import org.jboss.dna.common.util.CheckArg;
051    import org.jboss.dna.common.util.HashCode;
052    import org.jboss.dna.common.util.Logger;
053    import org.jboss.dna.graph.properties.NamespaceRegistry;
054    import org.jboss.dna.graph.properties.PropertyFactory;
055    import org.jboss.dna.graph.properties.ValueFactories;
056    import org.jboss.dna.repository.RepositoryI18n;
057    import org.jboss.dna.repository.observation.NodeChange;
058    import org.jboss.dna.repository.observation.NodeChangeListener;
059    import org.jboss.dna.repository.observation.NodeChanges;
060    import org.jboss.dna.repository.services.AbstractServiceAdministrator;
061    import org.jboss.dna.repository.services.AdministeredService;
062    import org.jboss.dna.repository.services.ServiceAdministrator;
063    import org.jboss.dna.repository.util.JcrExecutionContext;
064    import org.jboss.dna.repository.util.JcrTools;
065    import org.jboss.dna.repository.util.RepositoryNodePath;
066    import org.jboss.dna.repository.util.SessionFactory;
067    
068    /**
069     * A sequencing system is used to monitor changes in the content of {@link Repository JCR repositories} and to sequence the
070     * content to extract or to generate structured information.
071     * 
072     * @author Randall Hauch
073     * @author John Verhaeg
074     */
075    public class SequencingService implements AdministeredService, NodeChangeListener {
076    
077        /**
078         * Interface used to select the set of {@link Sequencer} instances that should be run.
079         * 
080         * @author Randall Hauch
081         */
082        public static interface Selector {
083    
084            /**
085             * Select the sequencers that should be used to sequence the supplied node.
086             * 
087             * @param sequencers the list of all sequencers available at the moment; never null
088             * @param node the node to be sequenced; never null
089             * @param nodeChange the set of node changes; never null
090             * @return the list of sequencers that should be used; may not be null
091             */
092            List<Sequencer> selectSequencers( List<Sequencer> sequencers,
093                                              Node node,
094                                              NodeChange nodeChange );
095        }
096    
097        /**
098         * The default {@link Selector} implementation that selects every sequencer every time it's called, regardless of the node (or
099         * logger) supplied.
100         * 
101         * @author Randall Hauch
102         */
103        protected static class DefaultSelector implements Selector {
104    
105            public List<Sequencer> selectSequencers( List<Sequencer> sequencers,
106                                                     Node node,
107                                                     NodeChange nodeChange ) {
108                return sequencers;
109            }
110        }
111    
112        /**
113         * Interface used to determine whether a {@link NodeChange} should be processed.
114         * 
115         * @author Randall Hauch
116         */
117        public static interface NodeFilter {
118    
119            /**
120             * Determine whether the node represented by the supplied change should be submitted for sequencing.
121             * 
122             * @param nodeChange the node change event
123             * @return true if the node should be submitted for sequencing, or false if the change should be ignored
124             */
125            boolean accept( NodeChange nodeChange );
126        }
127    
128        /**
129         * The default filter implementation, which accepts only new nodes or nodes that have new or changed properties.
130         * 
131         * @author Randall Hauch
132         */
133        protected static class DefaultNodeFilter implements NodeFilter {
134    
135            public boolean accept( NodeChange nodeChange ) {
136                // Only care about new nodes or nodes that have new/changed properies ...
137                return nodeChange.includesEventTypes(Event.NODE_ADDED, Event.PROPERTY_ADDED, Event.PROPERTY_CHANGED);
138            }
139        }
140    
141        /**
142         * The default {@link Selector} that considers every {@link Sequencer} to be used for every node.
143         * 
144         * @see SequencingService#setSequencerSelector(org.jboss.dna.repository.sequencers.SequencingService.Selector)
145         */
146        public static final Selector DEFAULT_SEQUENCER_SELECTOR = new DefaultSelector();
147        /**
148         * The default {@link NodeFilter} that accepts new nodes or nodes that have new/changed properties.
149         * 
150         * @see SequencingService#setSequencerSelector(org.jboss.dna.repository.sequencers.SequencingService.Selector)
151         */
152        public static final NodeFilter DEFAULT_NODE_FILTER = new DefaultNodeFilter();
153    
154        /**
155         * Class loader factory instance that always returns the {@link Thread#getContextClassLoader() current thread's context class
156         * loader} (if not null) or component library's class loader.
157         */
158        protected static final ClassLoaderFactory DEFAULT_CLASSLOADER_FACTORY = new StandardClassLoaderFactory(
159                                                                                                               SequencingService.class.getClassLoader());
160    
161        /**
162         * The administrative component for this service.
163         * 
164         * @author Randall Hauch
165         */
166        protected class Administrator extends AbstractServiceAdministrator {
167    
168            protected Administrator() {
169                super(RepositoryI18n.sequencingServiceName, State.PAUSED);
170            }
171    
172            /**
173             * {@inheritDoc}
174             */
175            @Override
176            protected void doStart( State fromState ) {
177                super.doStart(fromState);
178                startService();
179            }
180    
181            /**
182             * {@inheritDoc}
183             */
184            @Override
185            protected void doShutdown( State fromState ) {
186                super.doShutdown(fromState);
187                shutdownService();
188            }
189    
190            /**
191             * {@inheritDoc}
192             */
193            @Override
194            protected boolean doCheckIsTerminated() {
195                return isServiceTerminated();
196            }
197    
198            /**
199             * {@inheritDoc}
200             */
201            public boolean awaitTermination( long timeout,
202                                             TimeUnit unit ) throws InterruptedException {
203                return doAwaitTermination(timeout, unit);
204            }
205    
206        }
207    
208        private JcrExecutionContext executionContext;
209        private SequencerLibrary sequencerLibrary = new SequencerLibrary();
210        private Selector sequencerSelector = DEFAULT_SEQUENCER_SELECTOR;
211        private NodeFilter nodeFilter = DEFAULT_NODE_FILTER;
212        private ExecutorService executorService;
213        private final Statistics statistics = new Statistics();
214        private final Administrator administrator = new Administrator();
215    
216        /**
217         * Create a new sequencing system, configured with no sequencers and not monitoring any workspaces. Upon construction, the
218         * system is {@link ServiceAdministrator#isPaused() paused} and must be configured and then
219         * {@link ServiceAdministrator#start() started}.
220         */
221        public SequencingService() {
222            this.sequencerLibrary.setClassLoaderFactory(DEFAULT_CLASSLOADER_FACTORY);
223        }
224    
225        /**
226         * Return the administrative component for this service.
227         * 
228         * @return the administrative component; never null
229         */
230        public ServiceAdministrator getAdministrator() {
231            return this.administrator;
232        }
233    
234        /**
235         * Get the statistics for this system.
236         * 
237         * @return statistics
238         */
239        public Statistics getStatistics() {
240            return this.statistics;
241        }
242    
243        /**
244         * @return sequencerLibrary
245         */
246        protected ComponentLibrary<Sequencer, SequencerConfig> getSequencerLibrary() {
247            return this.sequencerLibrary;
248        }
249    
250        /**
251         * Add the configuration for a sequencer, or update any existing one that represents the
252         * {@link SequencerConfig#equals(Object) same configuration}
253         * 
254         * @param config the new configuration
255         * @return true if the sequencer was added, or false if there already was an existing and
256         *         {@link SequencerConfig#hasChanged(SequencerConfig) unchanged} sequencer configuration
257         * @throws IllegalArgumentException if <code>config</code> is null
258         * @see #updateSequencer(SequencerConfig)
259         * @see #removeSequencer(SequencerConfig)
260         */
261        public boolean addSequencer( SequencerConfig config ) {
262            return this.sequencerLibrary.add(config);
263        }
264    
265        /**
266         * Update the configuration for a sequencer, or add it if there is no {@link SequencerConfig#equals(Object) matching
267         * configuration}.
268         * 
269         * @param config the updated (or new) configuration
270         * @return true if the sequencer was updated, or false if there already was an existing and
271         *         {@link SequencerConfig#hasChanged(SequencerConfig) unchanged} sequencer configuration
272         * @throws IllegalArgumentException if <code>config</code> is null
273         * @see #addSequencer(SequencerConfig)
274         * @see #removeSequencer(SequencerConfig)
275         */
276        public boolean updateSequencer( SequencerConfig config ) {
277            return this.sequencerLibrary.update(config);
278        }
279    
280        /**
281         * Remove the configuration for a sequencer.
282         * 
283         * @param config the configuration to be removed
284         * @return true if the sequencer was removed, or false if there was no existing sequencer
285         * @throws IllegalArgumentException if <code>config</code> is null
286         * @see #addSequencer(SequencerConfig)
287         * @see #updateSequencer(SequencerConfig)
288         */
289        public boolean removeSequencer( SequencerConfig config ) {
290            return this.sequencerLibrary.remove(config);
291        }
292    
293        /**
294         * @return executionContext
295         */
296        public JcrExecutionContext getExecutionContext() {
297            return this.executionContext;
298        }
299    
300        /**
301         * @param executionContext Sets executionContext to the specified value.
302         */
303        public void setExecutionContext( JcrExecutionContext executionContext ) {
304            CheckArg.isNotNull(executionContext, "execution context");
305            if (this.getAdministrator().isStarted()) {
306                throw new IllegalStateException(RepositoryI18n.unableToChangeExecutionContextWhileRunning.text());
307            }
308            this.executionContext = executionContext;
309            this.sequencerLibrary.setClassLoaderFactory(executionContext);
310        }
311    
312        /**
313         * Get the executor service used to run the sequencers.
314         * 
315         * @return the executor service
316         * @see #setExecutorService(ExecutorService)
317         */
318        public ExecutorService getExecutorService() {
319            return this.executorService;
320        }
321    
322        /**
323         * Set the executor service that should be used by this system. By default, the system is set up with a
324         * {@link Executors#newSingleThreadExecutor() executor that uses a single thread}.
325         * 
326         * @param executorService the executor service
327         * @see #getExecutorService()
328         * @see Executors#newCachedThreadPool()
329         * @see Executors#newCachedThreadPool(java.util.concurrent.ThreadFactory)
330         * @see Executors#newFixedThreadPool(int)
331         * @see Executors#newFixedThreadPool(int, java.util.concurrent.ThreadFactory)
332         * @see Executors#newScheduledThreadPool(int)
333         * @see Executors#newScheduledThreadPool(int, java.util.concurrent.ThreadFactory)
334         * @see Executors#newSingleThreadExecutor()
335         * @see Executors#newSingleThreadExecutor(java.util.concurrent.ThreadFactory)
336         * @see Executors#newSingleThreadScheduledExecutor()
337         * @see Executors#newSingleThreadScheduledExecutor(java.util.concurrent.ThreadFactory)
338         */
339        public void setExecutorService( ExecutorService executorService ) {
340            CheckArg.isNotNull(executorService, "executor service");
341            if (this.getAdministrator().isStarted()) {
342                throw new IllegalStateException(RepositoryI18n.unableToChangeExecutionContextWhileRunning.text());
343            }
344            this.executorService = executorService;
345        }
346    
347        /**
348         * Override this method to creates a different kind of default executor service. This method is called when the system is
349         * {@link #startService() started} without an executor service being {@link #setExecutorService(ExecutorService) set}.
350         * <p>
351         * This method creates a {@link Executors#newSingleThreadExecutor() single-threaded executor}.
352         * </p>
353         * 
354         * @return the executor service
355         */
356        protected ExecutorService createDefaultExecutorService() {
357            return Executors.newSingleThreadExecutor();
358        }
359    
360        protected void startService() {
361            if (this.getExecutionContext() == null) {
362                throw new IllegalStateException(RepositoryI18n.unableToStartSequencingServiceWithoutExecutionContext.text());
363            }
364            if (this.executorService == null) {
365                this.executorService = createDefaultExecutorService();
366            }
367            assert this.executorService != null;
368            assert this.sequencerSelector != null;
369            assert this.nodeFilter != null;
370            assert this.sequencerLibrary != null;
371        }
372    
373        protected void shutdownService() {
374            if (this.executorService != null) {
375                this.executorService.shutdown();
376            }
377        }
378    
379        protected boolean isServiceTerminated() {
380            if (this.executorService != null) {
381                return this.executorService.isTerminated();
382            }
383            return true;
384        }
385    
386        protected boolean doAwaitTermination( long timeout,
387                                              TimeUnit unit ) throws InterruptedException {
388            if (this.executorService == null || this.executorService.isTerminated()) return true;
389            return this.executorService.awaitTermination(timeout, unit);
390        }
391    
392        /**
393         * Get the sequencing selector used by this system.
394         * 
395         * @return the sequencing selector
396         */
397        public Selector getSequencerSelector() {
398            return this.sequencerSelector;
399        }
400    
401        /**
402         * Set the sequencer selector, or null if the {@link #DEFAULT_SEQUENCER_SELECTOR default sequencer selector} should be used.
403         * 
404         * @param sequencerSelector the selector
405         */
406        public void setSequencerSelector( Selector sequencerSelector ) {
407            this.sequencerSelector = sequencerSelector != null ? sequencerSelector : DEFAULT_SEQUENCER_SELECTOR;
408        }
409    
410        /**
411         * Get the node filter used by this system.
412         * 
413         * @return the node filter
414         */
415        public NodeFilter getNodeFilter() {
416            return this.nodeFilter;
417        }
418    
419        /**
420         * Set the filter that checks which nodes are to be sequenced, or null if the {@link #DEFAULT_NODE_FILTER default node filter}
421         * should be used.
422         * 
423         * @param nodeFilter the new node filter
424         */
425        public void setNodeFilter( NodeFilter nodeFilter ) {
426            this.nodeFilter = nodeFilter != null ? nodeFilter : DEFAULT_NODE_FILTER;
427        }
428    
429        /**
430         * {@inheritDoc}
431         */
432        public void onNodeChanges( NodeChanges changes ) {
433            NodeFilter filter = this.getNodeFilter();
434            for (final NodeChange changedNode : changes) {
435                // Only care about new nodes or nodes that have new/changed properies ...
436                if (filter.accept(changedNode)) {
437                    try {
438                        this.executorService.execute(new Runnable() {
439    
440                            public void run() {
441                                processChangedNode(changedNode);
442                            }
443                        });
444                    } catch (RejectedExecutionException e) {
445                        // The executor service has been shut down, so do nothing with this set of changes
446                    }
447                }
448            }
449        }
450    
451        /**
452         * Do the work of processing by sequencing the node. This method is called by the {@link #executorService executor service}
453         * when it performs it's work on the enqueued {@link NodeChange NodeChange runnable objects}.
454         * 
455         * @param changedNode the node to be processed.
456         */
457        protected void processChangedNode( NodeChange changedNode ) {
458            final JcrExecutionContext context = this.getExecutionContext();
459            final Logger logger = context.getLogger(getClass());
460            assert logger != null;
461            try {
462                final String repositoryWorkspaceName = changedNode.getRepositoryWorkspaceName();
463                Session session = null;
464                try {
465                    // Figure out which sequencers accept this path,
466                    // and track which output nodes should be passed to each sequencer...
467                    final String nodePath = changedNode.getAbsolutePath();
468                    Map<SequencerCall, Set<RepositoryNodePath>> sequencerCalls = new HashMap<SequencerCall, Set<RepositoryNodePath>>();
469                    List<Sequencer> allSequencers = this.sequencerLibrary.getInstances();
470                    List<Sequencer> sequencers = new ArrayList<Sequencer>(allSequencers.size());
471                    for (Sequencer sequencer : allSequencers) {
472                        final SequencerConfig config = sequencer.getConfiguration();
473                        for (SequencerPathExpression pathExpression : config.getPathExpressions()) {
474                            for (String propertyName : changedNode.getModifiedProperties()) {
475                                String path = nodePath + "/@" + propertyName;
476                                SequencerPathExpression.Matcher matcher = pathExpression.matcher(path);
477                                if (matcher.matches()) {
478                                    // String selectedPath = matcher.getSelectedPath();
479                                    RepositoryNodePath outputPath = RepositoryNodePath.parse(matcher.getOutputPath(),
480                                                                                             repositoryWorkspaceName);
481                                    SequencerCall call = new SequencerCall(sequencer, propertyName);
482                                    // Record the output path ...
483                                    Set<RepositoryNodePath> outputPaths = sequencerCalls.get(call);
484                                    if (outputPaths == null) {
485                                        outputPaths = new HashSet<RepositoryNodePath>();
486                                        sequencerCalls.put(call, outputPaths);
487                                    }
488                                    outputPaths.add(outputPath);
489                                    sequencers.add(sequencer);
490                                    break;
491                                }
492                            }
493                        }
494                    }
495    
496                    Node node = null;
497                    if (!sequencers.isEmpty()) {
498                        // Create a session that we'll use for all sequencing ...
499                        session = context.getSessionFactory().createSession(repositoryWorkspaceName);
500    
501                        // Find the changed node ...
502                        String relPath = changedNode.getAbsolutePath().replaceAll("^/+", "");
503                        node = session.getRootNode().getNode(relPath);
504    
505                        // Figure out which sequencers should run ...
506                        sequencers = this.sequencerSelector.selectSequencers(sequencers, node, changedNode);
507                    }
508                    if (sequencers.isEmpty()) {
509                        this.statistics.recordNodeSkipped();
510                        if (logger.isDebugEnabled()) {
511                            logger.trace("Skipping '{0}': no sequencers matched this condition", changedNode);
512                        }
513                    } else {
514                        // Run each of those sequencers ...
515                        for (Map.Entry<SequencerCall, Set<RepositoryNodePath>> entry : sequencerCalls.entrySet()) {
516                            final SequencerCall sequencerCall = entry.getKey();
517                            final Set<RepositoryNodePath> outputPaths = entry.getValue();
518                            final Sequencer sequencer = sequencerCall.getSequencer();
519                            final String sequencerName = sequencer.getConfiguration().getName();
520                            final String propertyName = sequencerCall.getSequencedPropertyName();
521    
522                            // Get the paths to the nodes where the sequencer should write it's output ...
523                            assert outputPaths != null && outputPaths.size() != 0;
524    
525                            // Create a new execution context for each sequencer
526                            final Context executionContext = new Context(context);
527                            final SimpleProblems problems = new SimpleProblems();
528                            try {
529                                sequencer.execute(node, propertyName, changedNode, outputPaths, executionContext, problems);
530                            } catch (RepositoryException e) {
531                                logger.error(e, RepositoryI18n.errorInRepositoryWhileSequencingNode, sequencerName, changedNode);
532                            } catch (SequencerException e) {
533                                logger.error(e, RepositoryI18n.errorWhileSequencingNode, sequencerName, changedNode);
534                            } finally {
535                                try {
536                                    // Save the changes made by each sequencer ...
537                                    if (session != null) session.save();
538                                } finally {
539                                    // And always close the context.
540                                    // This closes all sessions that may have been created by the sequencer.
541                                    executionContext.close();
542                                }
543                            }
544                        }
545                        this.statistics.recordNodeSequenced();
546                    }
547                } finally {
548                    if (session != null) session.logout();
549                }
550            } catch (RepositoryException e) {
551                logger.error(e, RepositoryI18n.errorInRepositoryWhileFindingSequencersToRunAgainstNode, changedNode);
552            } catch (Throwable e) {
553                logger.error(e, RepositoryI18n.errorFindingSequencersToRunAgainstNode, changedNode);
554            }
555        }
556    
557        protected class Context implements JcrExecutionContext {
558    
559            protected final JcrExecutionContext delegate;
560            protected final SessionFactory factory;
561            private final Set<Session> sessions = new HashSet<Session>();
562            protected final AtomicBoolean closed = new AtomicBoolean(false);
563    
564            protected Context( JcrExecutionContext context ) {
565                this.delegate = context;
566                final SessionFactory delegateSessionFactory = this.delegate.getSessionFactory();
567                this.factory = new SessionFactory() {
568    
569                    public Session createSession( String name ) throws RepositoryException {
570                        if (closed.get()) throw new IllegalStateException(RepositoryI18n.executionContextHasBeenClosed.text());
571                        Session session = delegateSessionFactory.createSession(name);
572                        recordSession(session);
573                        return session;
574                    }
575                };
576            }
577    
578            public synchronized void close() {
579                if (this.closed.get()) return;
580                this.closed.set(true);
581                for (Session session : sessions) {
582                    if (session != null) session.logout();
583                }
584            }
585    
586            /**
587             * {@inheritDoc}
588             * 
589             * @see org.jboss.dna.common.component.ClassLoaderFactory#getClassLoader(java.lang.String[])
590             */
591            public ClassLoader getClassLoader( String... classpath ) {
592                return delegate.getClassLoader(classpath);
593            }
594    
595            /**
596             * {@inheritDoc}
597             * 
598             * @see org.jboss.dna.graph.ExecutionContext#getAccessControlContext()
599             */
600            public AccessControlContext getAccessControlContext() {
601                return delegate.getAccessControlContext();
602            }
603    
604            /**
605             * {@inheritDoc}
606             * 
607             * @see org.jboss.dna.graph.ExecutionContext#getLoginContext()
608             */
609            public LoginContext getLoginContext() {
610                return delegate.getLoginContext();
611            }
612    
613            /**
614             * {@inheritDoc}
615             */
616            public NamespaceRegistry getNamespaceRegistry() {
617                return this.delegate.getNamespaceRegistry();
618            }
619    
620            /**
621             * {@inheritDoc}
622             */
623            public PropertyFactory getPropertyFactory() {
624                return this.delegate.getPropertyFactory();
625            }
626    
627            /**
628             * {@inheritDoc}
629             */
630            public SessionFactory getSessionFactory() {
631                return this.factory;
632            }
633    
634            /**
635             * {@inheritDoc}
636             * 
637             * @see org.jboss.dna.graph.ExecutionContext#getSubject()
638             */
639            public Subject getSubject() {
640                return this.delegate.getSubject();
641            }
642    
643            /**
644             * {@inheritDoc}
645             */
646            public JcrTools getTools() {
647                return SequencingService.this.getExecutionContext().getTools();
648            }
649    
650            /**
651             * {@inheritDoc}
652             */
653            public ValueFactories getValueFactories() {
654                return this.delegate.getValueFactories();
655            }
656    
657            /**
658             * {@inheritDoc}
659             * 
660             * @see org.jboss.dna.graph.ExecutionContext#getLogger(java.lang.Class)
661             */
662            public Logger getLogger( Class<?> clazz ) {
663                return this.delegate.getLogger(clazz);
664            }
665    
666            /**
667             * {@inheritDoc}
668             * 
669             * @see org.jboss.dna.graph.ExecutionContext#getLogger(java.lang.String)
670             */
671            public Logger getLogger( String name ) {
672                return this.delegate.getLogger(name);
673            }
674    
675            protected synchronized void recordSession( Session session ) {
676                if (session != null) sessions.add(session);
677            }
678        }
679    
680        /**
681         * The statistics for the system. Each sequencing system has an instance of this class that is updated.
682         * 
683         * @author Randall Hauch
684         */
685        @ThreadSafe
686        public class Statistics {
687    
688            private final AtomicLong numberOfNodesSequenced = new AtomicLong(0);
689            private final AtomicLong numberOfNodesSkipped = new AtomicLong(0);
690            private final AtomicLong startTime;
691    
692            protected Statistics() {
693                startTime = new AtomicLong(System.currentTimeMillis());
694            }
695    
696            public Statistics reset() {
697                this.startTime.set(System.currentTimeMillis());
698                this.numberOfNodesSequenced.set(0);
699                this.numberOfNodesSkipped.set(0);
700                return this;
701            }
702    
703            /**
704             * @return the system time when the statistics were started
705             */
706            public long getStartTime() {
707                return this.startTime.get();
708            }
709    
710            /**
711             * @return the number of nodes that were sequenced
712             */
713            public long getNumberOfNodesSequenced() {
714                return this.numberOfNodesSequenced.get();
715            }
716    
717            /**
718             * @return the number of nodes that were skipped because no sequencers applied
719             */
720            public long getNumberOfNodesSkipped() {
721                return this.numberOfNodesSkipped.get();
722            }
723    
724            protected void recordNodeSequenced() {
725                this.numberOfNodesSequenced.incrementAndGet();
726            }
727    
728            protected void recordNodeSkipped() {
729                this.numberOfNodesSkipped.incrementAndGet();
730            }
731        }
732    
733        @Immutable
734        protected class SequencerCall {
735    
736            private final Sequencer sequencer;
737            private final String sequencerName;
738            private final String sequencedPropertyName;
739            private final int hc;
740    
741            protected SequencerCall( Sequencer sequencer,
742                                     String sequencedPropertyName ) {
743                this.sequencer = sequencer;
744                this.sequencerName = sequencer.getConfiguration().getName();
745                this.sequencedPropertyName = sequencedPropertyName;
746                this.hc = HashCode.compute(this.sequencerName, this.sequencedPropertyName);
747            }
748    
749            /**
750             * @return sequencer
751             */
752            public Sequencer getSequencer() {
753                return this.sequencer;
754            }
755    
756            /**
757             * @return sequencedPropertyName
758             */
759            public String getSequencedPropertyName() {
760                return this.sequencedPropertyName;
761            }
762    
763            /**
764             * {@inheritDoc}
765             */
766            @Override
767            public int hashCode() {
768                return this.hc;
769            }
770    
771            /**
772             * {@inheritDoc}
773             */
774            @Override
775            public boolean equals( Object obj ) {
776                if (obj == this) return true;
777                if (obj instanceof SequencerCall) {
778                    SequencerCall that = (SequencerCall)obj;
779                    if (!this.sequencerName.equals(that.sequencerName)) return false;
780                    if (!this.sequencedPropertyName.equals(that.sequencedPropertyName)) return false;
781                    return true;
782                }
783                return false;
784            }
785        }
786    }