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.connector.federation;
023    
024    import java.util.Collections;
025    import java.util.Enumeration;
026    import java.util.HashMap;
027    import java.util.Hashtable;
028    import java.util.LinkedList;
029    import java.util.List;
030    import java.util.Map;
031    import java.util.concurrent.TimeUnit;
032    import java.util.concurrent.atomic.AtomicInteger;
033    import javax.naming.Context;
034    import javax.naming.RefAddr;
035    import javax.naming.Reference;
036    import javax.naming.StringRefAddr;
037    import javax.naming.spi.ObjectFactory;
038    import javax.security.auth.callback.Callback;
039    import javax.security.auth.callback.CallbackHandler;
040    import javax.security.auth.callback.NameCallback;
041    import javax.security.auth.callback.PasswordCallback;
042    import javax.security.auth.login.LoginException;
043    import net.jcip.annotations.ThreadSafe;
044    import org.jboss.dna.common.collection.Problems;
045    import org.jboss.dna.common.collection.SimpleProblems;
046    import org.jboss.dna.common.i18n.I18n;
047    import org.jboss.dna.common.util.CheckArg;
048    import org.jboss.dna.common.util.Logger;
049    import org.jboss.dna.connector.federation.executor.FederatingCommandExecutor;
050    import org.jboss.dna.connector.federation.executor.SingleProjectionCommandExecutor;
051    import org.jboss.dna.graph.ExecutionContext;
052    import org.jboss.dna.graph.ExecutionContextFactory;
053    import org.jboss.dna.graph.cache.BasicCachePolicy;
054    import org.jboss.dna.graph.cache.CachePolicy;
055    import org.jboss.dna.graph.commands.GraphCommand;
056    import org.jboss.dna.graph.commands.basic.BasicCompositeCommand;
057    import org.jboss.dna.graph.commands.basic.BasicGetChildrenCommand;
058    import org.jboss.dna.graph.commands.basic.BasicGetNodeCommand;
059    import org.jboss.dna.graph.commands.executor.CommandExecutor;
060    import org.jboss.dna.graph.commands.executor.LoggingCommandExecutor;
061    import org.jboss.dna.graph.commands.executor.NoOpCommandExecutor;
062    import org.jboss.dna.graph.connectors.RepositoryConnection;
063    import org.jboss.dna.graph.connectors.RepositoryConnectionFactory;
064    import org.jboss.dna.graph.connectors.RepositoryContext;
065    import org.jboss.dna.graph.connectors.RepositorySource;
066    import org.jboss.dna.graph.connectors.RepositorySourceCapabilities;
067    import org.jboss.dna.graph.connectors.RepositorySourceException;
068    import org.jboss.dna.graph.properties.InvalidPathException;
069    import org.jboss.dna.graph.properties.Name;
070    import org.jboss.dna.graph.properties.NameFactory;
071    import org.jboss.dna.graph.properties.Path;
072    import org.jboss.dna.graph.properties.PathFactory;
073    import org.jboss.dna.graph.properties.Property;
074    import org.jboss.dna.graph.properties.ValueFactories;
075    import org.jboss.dna.graph.properties.ValueFactory;
076    
077    /**
078     * @author Randall Hauch
079     */
080    @ThreadSafe
081    public class FederatedRepositorySource implements RepositorySource, ObjectFactory {
082    
083        /**
084         */
085        private static final long serialVersionUID = 7587346948013486977L;
086    
087        /**
088         * The default limit is {@value} for retrying {@link RepositoryConnection connection} calls to the underlying source.
089         */
090        public static final int DEFAULT_RETRY_LIMIT = 0;
091    
092        public static final String DEFAULT_CONFIGURATION_SOURCE_PATH = "/";
093    
094        protected static final String REPOSITORY_NAME = "repositoryName";
095        protected static final String SOURCE_NAME = "sourceName";
096        protected static final String USERNAME = "username";
097        protected static final String PASSWORD = "password";
098        protected static final String CONFIGURATION_SOURCE_NAME = "configurationSourceName";
099        protected static final String CONFIGURATION_SOURCE_PATH = "configurationSourcePath";
100        protected static final String SECURITY_DOMAIN = "securityDomain";
101        protected static final String RETRY_LIMIT = "retryLimit";
102    
103        public static final String PATH_TO_CONFIGURATION_INFORMATION = "/dna:system/dna:federation";
104        public static final String DNA_CACHE_SEGMENT = "dna:cache";
105        public static final String DNA_PROJECTIONS_SEGMENT = "dna:projections";
106        public static final String PROJECTION_RULES_CONFIG_PROPERTY_NAME = "dna:projectionRules";
107        public static final String CACHE_POLICY_TIME_TO_LIVE_CONFIG_PROPERTY_NAME = "dna:timeToCache";
108    
109        private String repositoryName;
110        private String sourceName;
111        private String username;
112        private String password;
113        private String configurationSourceName;
114        private String configurationSourcePath = DEFAULT_CONFIGURATION_SOURCE_PATH;
115        private String securityDomain;
116        private final AtomicInteger retryLimit = new AtomicInteger(DEFAULT_RETRY_LIMIT);
117        private transient FederatedRepository repository;
118        private transient RepositoryContext repositoryContext;
119    
120        /**
121         * Create a new instance of the source, which must still be properly initialized with a {@link #setRepositoryName(String)
122         * repository name}.
123         */
124        public FederatedRepositorySource() {
125            super();
126        }
127    
128        /**
129         * Create a new instance of the source with the required repository name and federation service.
130         * 
131         * @param repositoryName the repository name
132         * @throws IllegalArgumentException if the federation service is null or the repository name is null or blank
133         */
134        public FederatedRepositorySource( String repositoryName ) {
135            super();
136            CheckArg.isNotNull(repositoryName, "repositoryName");
137            this.repositoryName = repositoryName;
138        }
139    
140        /**
141         * {@inheritDoc}
142         * 
143         * @see org.jboss.dna.graph.connectors.RepositorySource#initialize(org.jboss.dna.graph.connectors.RepositoryContext)
144         */
145        public void initialize( RepositoryContext context ) throws RepositorySourceException {
146            this.repositoryContext = context;
147        }
148    
149        /**
150         * @return repositoryContext
151         */
152        public RepositoryContext getRepositoryContext() {
153            return repositoryContext;
154        }
155    
156        /**
157         * {@inheritDoc}
158         */
159        public synchronized String getName() {
160            return sourceName;
161        }
162    
163        /**
164         * Set the name of this source.
165         * <p>
166         * This is a required property.
167         * </p>
168         * 
169         * @param sourceName the name of this repository source
170         * @see #setConfigurationSourceName(String)
171         * @see #setConfigurationSourcePath(String)
172         * @see #setPassword(String)
173         * @see #setUsername(String)
174         * @see #setRepositoryName(String)
175         * @see #setPassword(String)
176         * @see #setUsername(String)
177         * @see #setName(String)
178         */
179        public synchronized void setName( String sourceName ) {
180            if (this.sourceName == sourceName || this.sourceName != null && this.sourceName.equals(sourceName)) return; // unchanged
181            this.sourceName = sourceName;
182            changeRepositoryConfig();
183        }
184    
185        /**
186         * {@inheritDoc}
187         * 
188         * @see org.jboss.dna.graph.connectors.RepositorySource#getRetryLimit()
189         */
190        public int getRetryLimit() {
191            return retryLimit.get();
192        }
193    
194        /**
195         * {@inheritDoc}
196         * 
197         * @see org.jboss.dna.graph.connectors.RepositorySource#setRetryLimit(int)
198         */
199        public void setRetryLimit( int limit ) {
200            retryLimit.set(limit < 0 ? 0 : limit);
201        }
202    
203        /**
204         * Get the name in JNDI of a {@link RepositorySource} instance that should be used by the {@link FederatedRepository federated
205         * repository} as the configuration repository.
206         * <p>
207         * This is a required property.
208         * </p>
209         * 
210         * @return the JNDI name of the {@link RepositorySource} instance that should be used for the configuration, or null if the
211         *         federated repository instance is to be found in JNDI
212         * @see #setConfigurationSourceName(String)
213         */
214        public String getConfigurationSourceName() {
215            return configurationSourceName;
216        }
217    
218        /**
219         * Get the name of a {@link RepositorySource} instance that should be used by the {@link FederatedRepository federated
220         * repository} as the configuration repository. The instance will be retrieved from the {@link RepositoryConnectionFactory}
221         * instance from the {@link RepositoryContext#getRepositoryConnectionFactory() repository context} supplied during
222         * {@link RepositorySource#initialize(RepositoryContext) initialization}.
223         * <p>
224         * This is a required property.
225         * </p>
226         * 
227         * @param sourceName the name of the {@link RepositorySource} instance that should be used for the configuration, or null if
228         *        the federated repository instance is to be found in JNDI
229         * @see #getConfigurationSourceName()
230         * @see #setConfigurationSourcePath(String)
231         * @see #setPassword(String)
232         * @see #setUsername(String)
233         * @see #setRepositoryName(String)
234         * @see #setName(String)
235         */
236        public void setConfigurationSourceName( String sourceName ) {
237            if (this.configurationSourceName == sourceName || this.configurationSourceName != null
238                && this.configurationSourceName.equals(sourceName)) return; // unchanged
239            this.configurationSourceName = sourceName;
240            changeRepositoryConfig();
241        }
242    
243        /**
244         * Get the path in the source that will be subgraph below the <code>/dna:system</code> branch of the repository.
245         * <p>
246         * This is a required property.
247         * </p>
248         * 
249         * @return the string array of projection rules, or null if the projection rules haven't yet been set or if the federated
250         *         repository instance is to be found in JNDI
251         * @see #setConfigurationSourcePath(String)
252         */
253        public String getConfigurationSourcePath() {
254            return configurationSourcePath;
255        }
256    
257        /**
258         * Set the path in the source that will be subgraph below the <code>/dna:system</code> branch of the repository.
259         * <p>
260         * This is a required property.
261         * </p>
262         * 
263         * @param pathInSourceToConfigurationRoot the path within the configuration source to the node that should be the root of the
264         *        configuration information, or null if the path hasn't yet been set or if the federated repository instance is to be
265         *        found in JNDI
266         * @see #setConfigurationSourcePath(String)
267         * @see #setConfigurationSourceName(String)
268         * @see #setPassword(String)
269         * @see #setUsername(String)
270         * @see #setRepositoryName(String)
271         * @see #setName(String)
272         */
273        public void setConfigurationSourcePath( String pathInSourceToConfigurationRoot ) {
274            if (this.configurationSourcePath == pathInSourceToConfigurationRoot || this.configurationSourcePath != null
275                && this.configurationSourcePath.equals(pathInSourceToConfigurationRoot)) return;
276            this.configurationSourcePath = pathInSourceToConfigurationRoot != null ? pathInSourceToConfigurationRoot : DEFAULT_CONFIGURATION_SOURCE_PATH;
277            changeRepositoryConfig();
278        }
279    
280        /**
281         * Get the name of the security domain that should be used by JAAS to identify the application or security context. This
282         * should correspond to the JAAS login configuration located within the JAAS login configuration file.
283         * 
284         * @return securityDomain
285         */
286        public String getSecurityDomain() {
287            return securityDomain;
288        }
289    
290        /**
291         * Set the name of the security domain that should be used by JAAS to identify the application or security context. This
292         * should correspond to the JAAS login configuration located within the JAAS login configuration file.
293         * 
294         * @param securityDomain Sets securityDomain to the specified value.
295         */
296        public void setSecurityDomain( String securityDomain ) {
297            if (this.securityDomain != null && this.securityDomain.equals(securityDomain)) return; // unchanged
298            this.securityDomain = securityDomain;
299            changeRepositoryConfig();
300        }
301    
302        /**
303         * Get the name of the federated repository.
304         * <p>
305         * This is a required property.
306         * </p>
307         * 
308         * @return the name of the repository
309         * @see #setRepositoryName(String)
310         */
311        public synchronized String getRepositoryName() {
312            return this.repositoryName;
313        }
314    
315        /**
316         * Get the name of the federated repository.
317         * <p>
318         * This is a required property.
319         * </p>
320         * 
321         * @param repositoryName the new name of the repository
322         * @throws IllegalArgumentException if the repository name is null, empty or blank
323         * @see #getRepositoryName()
324         * @see #setConfigurationSourceName(String)
325         * @see #setConfigurationSourcePath(String)
326         * @see #setPassword(String)
327         * @see #setUsername(String)
328         * @see #setName(String)
329         */
330        public synchronized void setRepositoryName( String repositoryName ) {
331            CheckArg.isNotEmpty(repositoryName, "repositoryName");
332            if (this.repositoryName != null && this.repositoryName.equals(repositoryName)) return; // unchanged
333            this.repositoryName = repositoryName;
334            changeRepositoryConfig();
335        }
336    
337        /**
338         * Get the username that should be used when authenticating and {@link #getConnection() creating connections}.
339         * <p>
340         * This is an optional property, required only when authentication is to be used.
341         * </p>
342         * 
343         * @return the username, or null if no username has been set or are not to be used
344         * @see #setUsername(String)
345         */
346        public String getUsername() {
347            return this.username;
348        }
349    
350        /**
351         * Set the username that should be used when authenticating and {@link #getConnection() creating connections}.
352         * <p>
353         * This is an optional property, required only when authentication is to be used.
354         * </p>
355         * 
356         * @param username the username, or null if no username has been set or are not to be used
357         * @see #getUsername()
358         * @see #setPassword(String)
359         * @see #setConfigurationSourceName(String)
360         * @see #setConfigurationSourcePath(String)
361         * @see #setPassword(String)
362         * @see #setRepositoryName(String)
363         * @see #setName(String)
364         */
365        public void setUsername( String username ) {
366            if (this.username != null && this.username.equals(username)) return; // unchanged
367            this.username = username;
368            changeRepositoryConfig();
369        }
370    
371        /**
372         * Get the password that should be used when authenticating and {@link #getConnection() creating connections}.
373         * <p>
374         * This is an optional property, required only when authentication is to be used.
375         * </p>
376         * 
377         * @return the password, or null if no password have been set or are not to be used
378         * @see #setPassword(String)
379         */
380        public String getPassword() {
381            return this.password;
382        }
383    
384        /**
385         * Get the password that should be used when authenticating and {@link #getConnection() creating connections}.
386         * <p>
387         * This is an optional property, required only when authentication is to be used.
388         * </p>
389         * 
390         * @param password the password, or null if no password have been set or are not to be used
391         * @see #getPassword()
392         * @see #setConfigurationSourceName(String)
393         * @see #setConfigurationSourcePath(String)
394         * @see #setUsername(String)
395         * @see #setRepositoryName(String)
396         * @see #setName(String)
397         */
398        public void setPassword( String password ) {
399            if (this.password != null && this.password.equals(password)) return; // unchanged
400            this.password = password;
401            changeRepositoryConfig();
402        }
403    
404        /**
405         * This method is called to signal that some aspect of the configuration has changed. If a {@link #getRepository() repository}
406         * instance has been created, it's configuration is
407         * {@link #getRepositoryConfiguration(ExecutionContext, RepositoryConnectionFactory) rebuilt} and updated. Nothing is done,
408         * however, if there is currently no {@link #getRepository() repository}.
409         */
410        protected synchronized void changeRepositoryConfig() {
411            if (this.repository != null) {
412                RepositoryContext repositoryContext = getRepositoryContext();
413                if (repositoryContext != null) {
414                    // Find in JNDI the repository source registry and the environment ...
415                    ExecutionContext context = getExecutionContext();
416                    RepositoryConnectionFactory factory = getRepositoryContext().getRepositoryConnectionFactory();
417                    // Compute a new repository config and set it on the repository ...
418                    FederatedRepositoryConfig newConfig = getRepositoryConfiguration(context, factory);
419                    this.repository.setConfiguration(newConfig);
420                }
421            }
422        }
423    
424        /**
425         * {@inheritDoc}
426         * 
427         * @see org.jboss.dna.graph.connectors.RepositorySource#getConnection()
428         */
429        public RepositoryConnection getConnection() throws RepositorySourceException {
430            if (getName() == null) {
431                I18n msg = FederationI18n.propertyIsRequired;
432                throw new RepositorySourceException(getName(), msg.text("name"));
433            }
434            if (getRepositoryContext() == null) {
435                I18n msg = FederationI18n.propertyIsRequired;
436                throw new RepositorySourceException(getName(), msg.text("repository context"));
437            }
438            if (getUsername() != null && getSecurityDomain() == null) {
439                I18n msg = FederationI18n.propertyIsRequired;
440                throw new RepositorySourceException(getName(), msg.text("security domain"));
441            }
442            // Find the repository ...
443            FederatedRepository repository = getRepository();
444            // Authenticate the user ...
445            String username = this.username;
446            Object credentials = this.password;
447            RepositoryConnection connection = repository.createConnection(this, username, credentials);
448            if (connection == null) {
449                I18n msg = FederationI18n.unableToAuthenticateConnectionToFederatedRepository;
450                throw new RepositorySourceException(msg.text(this.repositoryName, username));
451            }
452            // Return the new connection ...
453            return connection;
454        }
455    
456        /**
457         * Get the {@link FederatedRepository} instance that this source is using. This method uses the following logic:
458         * <ol>
459         * <li>If a {@link FederatedRepository} already was obtained from a prior call, the same instance is returned.</li>
460         * <li>A {@link FederatedRepository} is created using a {@link FederatedRepositoryConfig} is created from this instance's
461         * properties and {@link ExecutionContext} and {@link RepositoryConnectionFactory} instances obtained from JNDI.</li>
462         * <li></li>
463         * <li></li>
464         * </ol>
465         * 
466         * @return the federated repository instance
467         * @throws RepositorySourceException
468         */
469        protected synchronized FederatedRepository getRepository() throws RepositorySourceException {
470            if (repository == null) {
471                ExecutionContext context = getExecutionContext();
472                RepositoryConnectionFactory connectionFactory = getRepositoryContext().getRepositoryConnectionFactory();
473                // And create the configuration and the repository ...
474                FederatedRepositoryConfig config = getRepositoryConfiguration(context, connectionFactory);
475                repository = new FederatedRepository(context, connectionFactory, config);
476            }
477            return repository;
478        }
479    
480        protected ExecutionContext getExecutionContext() {
481            ExecutionContextFactory factory = getRepositoryContext().getExecutionContextFactory();
482            CallbackHandler handler = createCallbackHandler();
483            try {
484                String securityDomain = getSecurityDomain();
485                if (securityDomain != null || getUsername() != null) {
486                    return factory.create(securityDomain, handler);
487                }
488                return factory.create();
489            } catch (LoginException e) {
490                I18n msg = FederationI18n.unableToCreateExecutionContext;
491                throw new RepositorySourceException(getName(), msg.text(this.sourceName, securityDomain), e);
492            }
493        }
494    
495        protected CallbackHandler createCallbackHandler() {
496            return new CallbackHandler() {
497                public void handle( Callback[] callbacks ) {
498                    for (Callback callback : callbacks) {
499                        if (callback instanceof NameCallback) {
500                            NameCallback nameCallback = (NameCallback)callback;
501                            nameCallback.setName(FederatedRepositorySource.this.getUsername());
502                        }
503                        if (callback instanceof PasswordCallback) {
504                            PasswordCallback passwordCallback = (PasswordCallback)callback;
505                            passwordCallback.setPassword(FederatedRepositorySource.this.getPassword().toCharArray());
506                        }
507                    }
508                }
509            };
510        }
511    
512        /**
513         * Create a {@link FederatedRepositoryConfig} instance from the current properties of this instance. This method does
514         * <i>not</i> modify the state of this instance.
515         * 
516         * @param context the execution context that should be used to read the configuration; may not be null
517         * @param connectionFactory the factory for {@link RepositoryConnection}s can be obtained; may not be null
518         * @return a configuration reflecting the current state of this instance
519         */
520        protected synchronized FederatedRepositoryConfig getRepositoryConfiguration( ExecutionContext context,
521                                                                                     RepositoryConnectionFactory connectionFactory ) {
522            Problems problems = new SimpleProblems();
523            ValueFactories valueFactories = context.getValueFactories();
524            PathFactory pathFactory = valueFactories.getPathFactory();
525            NameFactory nameFactory = valueFactories.getNameFactory();
526            ValueFactory<Long> longFactory = valueFactories.getLongFactory();
527    
528            // Create the configuration projection ...
529            ProjectionParser projectionParser = ProjectionParser.getInstance();
530            String ruleStr = "/dna:system => " + this.getConfigurationSourcePath();
531            Projection.Rule[] rules = projectionParser.rulesFromStrings(context, ruleStr);
532            Projection configurationProjection = new Projection(this.getConfigurationSourceName(), rules);
533    
534            // Create a federating command executor to execute the commands and merge the results into a single set of
535            // commands.
536            final String configurationSourceName = configurationProjection.getSourceName();
537            List<Projection> projections = Collections.singletonList(configurationProjection);
538            CommandExecutor executor = null;
539            if (configurationProjection.getRules().size() == 0) {
540                // There is no projection for the configuration repository, so just use a no-op executor
541                executor = new NoOpCommandExecutor(context, configurationSourceName);
542            } else if (configurationProjection.isSimple()) {
543                // There is just a single projection for the configuration repository, so just use an executor that
544                // translates the paths using the projection
545                executor = new SingleProjectionCommandExecutor(context, configurationSourceName, configurationProjection,
546                                                               connectionFactory);
547            } else {
548                // The configuration repository has more than one projection, so we need to merge the results
549                executor = new FederatingCommandExecutor(context, configurationSourceName, projections, connectionFactory);
550            }
551            // Wrap the executor with a logging executor ...
552            executor = new LoggingCommandExecutor(executor, context.getLogger(getClass()), Logger.Level.DEBUG);
553    
554            // The configuration projection (via "executor") will convert this path into a path that exists in the configuration
555            // repository
556            Path configNode = pathFactory.create(PATH_TO_CONFIGURATION_INFORMATION);
557    
558            try {
559                // Get the repository node ...
560                BasicGetNodeCommand getRepository = new BasicGetNodeCommand(configNode);
561                executor.execute(getRepository);
562                if (getRepository.hasError()) {
563                    throw new FederationException(FederationI18n.federatedRepositoryCannotBeFound.text(repositoryName));
564                }
565    
566                // Get the first child node of the "dna:cache" node, since this represents the source used as the cache ...
567                Path cacheNode = pathFactory.create(configNode, nameFactory.create(DNA_CACHE_SEGMENT));
568                BasicGetChildrenCommand getCacheSource = new BasicGetChildrenCommand(cacheNode);
569    
570                executor.execute(getCacheSource);
571                if (getCacheSource.hasError() || getCacheSource.getChildren().size() < 1) {
572                    I18n msg = FederationI18n.requiredNodeDoesNotExistRelativeToNode;
573                    throw new FederationException(msg.text(DNA_CACHE_SEGMENT, configNode));
574                }
575    
576                // Add a command to get the projection defining the cache ...
577                Path pathToCacheRegion = pathFactory.create(cacheNode, getCacheSource.getChildren().get(0));
578                BasicGetNodeCommand getCacheRegion = new BasicGetNodeCommand(pathToCacheRegion);
579                executor.execute(getCacheRegion);
580                Projection cacheProjection = createProjection(context,
581                                                              projectionParser,
582                                                              getCacheRegion.getPath(),
583                                                              getCacheRegion.getPropertiesByName(),
584                                                              problems);
585    
586                if (getCacheRegion.hasError()) {
587                    I18n msg = FederationI18n.requiredNodeDoesNotExistRelativeToNode;
588                    throw new FederationException(msg.text(DNA_CACHE_SEGMENT, configNode));
589                }
590    
591                // Get the source projections for the repository ...
592                Path projectionsNode = pathFactory.create(configNode, nameFactory.create(DNA_PROJECTIONS_SEGMENT));
593                BasicGetChildrenCommand getProjections = new BasicGetChildrenCommand(projectionsNode);
594    
595                executor.execute(getProjections);
596                if (getProjections.hasError()) {
597                    I18n msg = FederationI18n.requiredNodeDoesNotExistRelativeToNode;
598                    throw new FederationException(msg.text(DNA_PROJECTIONS_SEGMENT, configNode));
599                }
600    
601                // Build the commands to get each of the projections (children of the "dna:projections" node) ...
602                List<Projection> sourceProjections = new LinkedList<Projection>();
603                if (getProjections.hasNoError() && !getProjections.getChildren().isEmpty()) {
604                    BasicCompositeCommand commands = new BasicCompositeCommand();
605                    for (Path.Segment child : getProjections.getChildren()) {
606                        final Path pathToSource = pathFactory.create(projectionsNode, child);
607                        commands.add(new BasicGetNodeCommand(pathToSource));
608                    }
609                    // Now execute these commands ...
610                    executor.execute(commands);
611    
612                    // Iterate over each region node obtained ...
613                    for (GraphCommand command : commands) {
614                        BasicGetNodeCommand getProjectionCommand = (BasicGetNodeCommand)command;
615                        if (getProjectionCommand.hasNoError()) {
616                            Projection projection = createProjection(context,
617                                                                     projectionParser,
618                                                                     getProjectionCommand.getPath(),
619                                                                     getProjectionCommand.getPropertiesByName(),
620                                                                     problems);
621                            if (projection != null) {
622                                Logger logger = context.getLogger(getClass());
623                                if (logger.isTraceEnabled()) {
624                                    logger.trace("Adding projection to federated repository {0}: {1}",
625                                                 getRepositoryName(),
626                                                 projection);
627                                }
628                                sourceProjections.add(projection);
629                            }
630                        }
631                    }
632                }
633    
634                // Look for the default cache policy ...
635                BasicCachePolicy cachePolicy = new BasicCachePolicy();
636                Property timeToLiveProperty = getRepository.getPropertiesByName().get(nameFactory.create(CACHE_POLICY_TIME_TO_LIVE_CONFIG_PROPERTY_NAME));
637                if (timeToLiveProperty != null && !timeToLiveProperty.isEmpty()) {
638                    cachePolicy.setTimeToLive(longFactory.create(timeToLiveProperty.getValues().next()), TimeUnit.MILLISECONDS);
639                }
640                CachePolicy defaultCachePolicy = cachePolicy.isEmpty() ? null : cachePolicy.getUnmodifiable();
641                return new FederatedRepositoryConfig(repositoryName, cacheProjection, sourceProjections, defaultCachePolicy);
642            } catch (InvalidPathException err) {
643                I18n msg = FederationI18n.federatedRepositoryCannotBeFound;
644                throw new FederationException(msg.text(repositoryName));
645            } finally {
646                executor.close();
647            }
648    
649        }
650    
651        /**
652         * Instantiate the {@link Projection} described by the supplied properties.
653         * 
654         * @param context the execution context that should be used to read the configuration; may not be null
655         * @param projectionParser the projection rule parser that should be used; may not be null
656         * @param path the path to the node where these properties were found; never null
657         * @param properties the properties; never null
658         * @param problems the problems container in which any problems should be reported; never null
659         * @return the region instance, or null if it could not be created
660         */
661        protected Projection createProjection( ExecutionContext context,
662                                               ProjectionParser projectionParser,
663                                               Path path,
664                                               Map<Name, Property> properties,
665                                               Problems problems ) {
666            ValueFactories valueFactories = context.getValueFactories();
667            NameFactory nameFactory = valueFactories.getNameFactory();
668            ValueFactory<String> stringFactory = valueFactories.getStringFactory();
669    
670            String sourceName = path.getLastSegment().getName().getLocalName();
671    
672            // Get the rules ...
673            Projection.Rule[] projectionRules = null;
674            Property projectionRulesProperty = properties.get(nameFactory.create(PROJECTION_RULES_CONFIG_PROPERTY_NAME));
675            if (projectionRulesProperty != null && !projectionRulesProperty.isEmpty()) {
676                String[] projectionRuleStrs = stringFactory.create(projectionRulesProperty.getValuesAsArray());
677                if (projectionRuleStrs != null && projectionRuleStrs.length != 0) {
678                    projectionRules = projectionParser.rulesFromStrings(context, projectionRuleStrs);
679                }
680            }
681            if (problems.hasErrors()) return null;
682    
683            Projection region = new Projection(sourceName, projectionRules);
684            return region;
685        }
686    
687        /**
688         * {@inheritDoc}
689         */
690        public synchronized Reference getReference() {
691            String className = getClass().getName();
692            String factoryClassName = this.getClass().getName();
693            Reference ref = new Reference(className, factoryClassName, null);
694    
695            if (getRepositoryName() != null) {
696                ref.add(new StringRefAddr(REPOSITORY_NAME, getRepositoryName()));
697            }
698            if (getName() != null) {
699                ref.add(new StringRefAddr(SOURCE_NAME, getName()));
700            }
701            if (getUsername() != null) {
702                ref.add(new StringRefAddr(USERNAME, getUsername()));
703            }
704            if (getPassword() != null) {
705                ref.add(new StringRefAddr(PASSWORD, getPassword()));
706            }
707            if (getConfigurationSourceName() != null) {
708                ref.add(new StringRefAddr(CONFIGURATION_SOURCE_NAME, getConfigurationSourceName()));
709            }
710            if (getConfigurationSourcePath() != null) {
711                ref.add(new StringRefAddr(CONFIGURATION_SOURCE_PATH, getConfigurationSourcePath()));
712            }
713            if (getSecurityDomain() != null) {
714                ref.add(new StringRefAddr(SECURITY_DOMAIN, getSecurityDomain()));
715            }
716            ref.add(new StringRefAddr(RETRY_LIMIT, Integer.toString(getRetryLimit())));
717            return ref;
718        }
719    
720        /**
721         * {@inheritDoc}
722         */
723        public Object getObjectInstance( Object obj,
724                                         javax.naming.Name name,
725                                         Context nameCtx,
726                                         Hashtable<?, ?> environment ) throws Exception {
727            if (obj instanceof Reference) {
728                Map<String, String> values = new HashMap<String, String>();
729                Reference ref = (Reference)obj;
730                Enumeration<?> en = ref.getAll();
731                while (en.hasMoreElements()) {
732                    RefAddr subref = (RefAddr)en.nextElement();
733                    if (subref instanceof StringRefAddr) {
734                        String key = subref.getType();
735                        Object value = subref.getContent();
736                        if (value != null) values.put(key, value.toString());
737                    }
738                }
739                String repositoryName = values.get(FederatedRepositorySource.REPOSITORY_NAME);
740                String sourceName = values.get(FederatedRepositorySource.SOURCE_NAME);
741                String username = values.get(FederatedRepositorySource.USERNAME);
742                String password = values.get(FederatedRepositorySource.PASSWORD);
743                String configurationSourceName = values.get(FederatedRepositorySource.CONFIGURATION_SOURCE_NAME);
744                String configurationSourcePath = values.get(FederatedRepositorySource.CONFIGURATION_SOURCE_PATH);
745                String securityDomain = values.get(FederatedRepositorySource.SECURITY_DOMAIN);
746                String retryLimit = values.get(FederatedRepositorySource.RETRY_LIMIT);
747    
748                // Create the source instance ...
749                FederatedRepositorySource source = new FederatedRepositorySource();
750                if (repositoryName != null) source.setRepositoryName(repositoryName);
751                if (sourceName != null) source.setName(sourceName);
752                if (username != null) source.setUsername(username);
753                if (password != null) source.setPassword(password);
754                if (configurationSourceName != null) source.setConfigurationSourceName(configurationSourceName);
755                if (configurationSourcePath != null) source.setConfigurationSourcePath(configurationSourcePath);
756                if (securityDomain != null) source.setSecurityDomain(securityDomain);
757                if (retryLimit != null) source.setRetryLimit(Integer.parseInt(retryLimit));
758                return source;
759            }
760            return null;
761        }
762    
763        /**
764         * {@inheritDoc}
765         */
766        @Override
767        public int hashCode() {
768            return repositoryName.hashCode();
769        }
770    
771        /**
772         * {@inheritDoc}
773         */
774        @Override
775        public boolean equals( Object obj ) {
776            if (obj == this) return true;
777            if (obj instanceof FederatedRepositorySource) {
778                FederatedRepositorySource that = (FederatedRepositorySource)obj;
779                // The repository name, source name, and federation service must all match
780                if (!this.getRepositoryName().equals(that.getRepositoryName())) return false;
781                if (this.getName() == null) {
782                    if (that.getName() != null) return false;
783                } else {
784                    if (!this.getName().equals(that.getName())) return false;
785                }
786                return true;
787            }
788            return false;
789        }
790    
791        /**
792         * {@inheritDoc}
793         * 
794         * @see org.jboss.dna.graph.connectors.RepositorySource#getCapabilities()
795         */
796        public RepositorySourceCapabilities getCapabilities() {
797            return new Capabilities();
798        }
799    
800        protected class Capabilities implements RepositorySourceCapabilities {
801            public boolean supportsSameNameSiblings() {
802                return true;
803            }
804    
805            public boolean supportsUpdates() {
806                return true;
807            }
808        }
809    
810    }