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