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