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     * Unless otherwise indicated, all code in JBoss DNA is licensed
010     * 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.graph.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.ExecutorService;
033    import java.util.concurrent.Executors;
034    import java.util.concurrent.TimeUnit;
035    import javax.naming.Context;
036    import javax.naming.Name;
037    import javax.naming.RefAddr;
038    import javax.naming.Reference;
039    import javax.naming.StringRefAddr;
040    import javax.naming.spi.ObjectFactory;
041    import net.jcip.annotations.GuardedBy;
042    import org.jboss.dna.common.i18n.I18n;
043    import org.jboss.dna.common.util.HashCode;
044    import org.jboss.dna.graph.DnaLexicon;
045    import org.jboss.dna.graph.ExecutionContext;
046    import org.jboss.dna.graph.GraphI18n;
047    import org.jboss.dna.graph.Location;
048    import org.jboss.dna.graph.Node;
049    import org.jboss.dna.graph.Subgraph;
050    import org.jboss.dna.graph.SubgraphNode;
051    import org.jboss.dna.graph.cache.BasicCachePolicy;
052    import org.jboss.dna.graph.cache.CachePolicy;
053    import org.jboss.dna.graph.connector.RepositoryConnection;
054    import org.jboss.dna.graph.connector.RepositoryConnectionFactory;
055    import org.jboss.dna.graph.connector.RepositoryContext;
056    import org.jboss.dna.graph.connector.RepositorySource;
057    import org.jboss.dna.graph.connector.RepositorySourceCapabilities;
058    import org.jboss.dna.graph.connector.RepositorySourceException;
059    import org.jboss.dna.graph.observe.Observer;
060    import org.jboss.dna.graph.property.NamespaceRegistry;
061    import org.jboss.dna.graph.property.Path;
062    import org.jboss.dna.graph.property.Property;
063    import org.jboss.dna.graph.property.ValueFactories;
064    import org.jboss.dna.graph.property.ValueFactory;
065    
066    /**
067     * 
068     */
069    public class FederatedRepositorySource implements RepositorySource, ObjectFactory {
070    
071        /**
072         * The default limit is {@value} for retrying {@link RepositoryConnection connection} calls to the underlying source.
073         */
074        public static final int DEFAULT_RETRY_LIMIT = 0;
075    
076        protected static final String SOURCE_NAME = "sourceName";
077        protected static final String RETRY_LIMIT = "retryLimit";
078    
079        private static final long serialVersionUID = 1L;
080    
081        private volatile String name;
082        private volatile int retryLimit;
083        private volatile RepositorySourceCapabilities capabilities = new RepositorySourceCapabilities(true, true, false, false, true);
084        private volatile transient FederatedRepository configuration;
085        private volatile transient RepositoryContext context;
086    
087        /**
088         * Construct a new instance of a {@link RepositorySource} for a federated repository.
089         */
090        public FederatedRepositorySource() {
091        }
092    
093        /**
094         * {@inheritDoc}
095         * 
096         * @see org.jboss.dna.graph.connector.RepositorySource#getName()
097         */
098        public String getName() {
099            return name;
100        }
101    
102        /**
103         * @param name Sets name to the specified value.
104         */
105        public synchronized void setName( String name ) {
106            if (this.name == name || this.name != null && this.name.equals(name)) return; // unchanged
107            this.name = name;
108            changeConfiguration();
109        }
110    
111        /**
112         * {@inheritDoc}
113         * 
114         * @see org.jboss.dna.graph.connector.RepositorySource#getRetryLimit()
115         */
116        public int getRetryLimit() {
117            return retryLimit;
118        }
119    
120        /**
121         * {@inheritDoc}
122         * 
123         * @see org.jboss.dna.graph.connector.RepositorySource#setRetryLimit(int)
124         */
125        public synchronized void setRetryLimit( int limit ) {
126            retryLimit = limit < 0 ? 0 : limit;
127            changeConfiguration();
128        }
129    
130        /**
131         * {@inheritDoc}
132         * 
133         * @see org.jboss.dna.graph.connector.RepositorySource#getCapabilities()
134         */
135        public RepositorySourceCapabilities getCapabilities() {
136            return capabilities;
137        }
138    
139        /**
140         * {@inheritDoc}
141         * 
142         * @see org.jboss.dna.graph.connector.RepositorySource#initialize(org.jboss.dna.graph.connector.RepositoryContext)
143         */
144        public synchronized void initialize( RepositoryContext context ) throws RepositorySourceException {
145            this.context = context;
146            changeConfiguration();
147        }
148    
149        /**
150         * Get the repository context that was used to {@link #initialize(RepositoryContext) initialize} this source.
151         * 
152         * @return the context, or null if the source was not yet {@link #initialize(RepositoryContext) initialized}
153         */
154        /*package*/RepositoryContext getRepositoryContext() {
155            return context;
156        }
157    
158        /**
159         * {@inheritDoc}
160         * 
161         * @see org.jboss.dna.graph.connector.RepositorySource#getConnection()
162         */
163        public RepositoryConnection getConnection() throws RepositorySourceException {
164            FederatedRepository config = this.configuration;
165            if (config == null) {
166                synchronized (this) {
167                    if (this.configuration == null) {
168                        // Check all the properties of this source ...
169                        String name = getName();
170                        if (name == null) {
171                            I18n msg = GraphI18n.namePropertyIsRequiredForFederatedRepositorySource;
172                            throw new RepositorySourceException(getName(), msg.text("name"));
173                        }
174                        RepositoryContext repositoryContext = getRepositoryContext();
175                        if (repositoryContext == null) {
176                            I18n msg = GraphI18n.federatedRepositorySourceMustBeInitialized;
177                            throw new RepositorySourceException(getName(), msg.text("name", name));
178                        }
179    
180                        // Load the configuration ...
181                        this.configuration = loadRepository(name, repositoryContext);
182                    }
183                    config = this.configuration;
184                }
185            }
186            Observer observer = this.context != null ? this.context.getObserver() : null;
187            return new FederatedRepositoryConnection(config, observer);
188        }
189    
190        /**
191         * {@inheritDoc}
192         * 
193         * @see javax.naming.Referenceable#getReference()
194         */
195        public Reference getReference() {
196            String className = getClass().getName();
197            String factoryClassName = this.getClass().getName();
198            Reference ref = new Reference(className, factoryClassName, null);
199    
200            ref.add(new StringRefAddr(SOURCE_NAME, getName()));
201            ref.add(new StringRefAddr(RETRY_LIMIT, Integer.toString(getRetryLimit())));
202            return ref;
203        }
204    
205        /**
206         * {@inheritDoc}
207         * 
208         * @see javax.naming.spi.ObjectFactory#getObjectInstance(java.lang.Object, javax.naming.Name, javax.naming.Context,
209         *      java.util.Hashtable)
210         */
211        public Object getObjectInstance( Object obj,
212                                         Name name,
213                                         Context nameCtx,
214                                         Hashtable<?, ?> environment ) throws Exception {
215            if (obj instanceof Reference) {
216                Map<String, String> values = new HashMap<String, String>();
217                Reference ref = (Reference)obj;
218                Enumeration<?> en = ref.getAll();
219                while (en.hasMoreElements()) {
220                    RefAddr subref = (RefAddr)en.nextElement();
221                    if (subref instanceof StringRefAddr) {
222                        String key = subref.getType();
223                        Object value = subref.getContent();
224                        if (value != null) values.put(key, value.toString());
225                    }
226                }
227                String sourceName = values.get(SOURCE_NAME);
228                String retryLimit = values.get(RETRY_LIMIT);
229    
230                // Create the source instance ...
231                FederatedRepositorySource source = new FederatedRepositorySource();
232                if (sourceName != null) source.setName(sourceName);
233                if (retryLimit != null) source.setRetryLimit(Integer.parseInt(retryLimit));
234                return source;
235            }
236            return null;
237        }
238    
239        /**
240         * {@inheritDoc}
241         */
242        @Override
243        public int hashCode() {
244            return HashCode.compute(getName());
245        }
246    
247        /**
248         * {@inheritDoc}
249         */
250        @Override
251        public boolean equals( Object obj ) {
252            if (obj == this) return true;
253            if (obj instanceof FederatedRepositorySource) {
254                FederatedRepositorySource that = (FederatedRepositorySource)obj;
255                // The source name must match
256                if (this.getName() == null) {
257                    if (that.getName() != null) return false;
258                } else {
259                    if (!this.getName().equals(that.getName())) return false;
260                }
261                return true;
262            }
263            return false;
264        }
265    
266        /**
267         * Mark the current configuration (if there is one) as being invalid.
268         */
269        @GuardedBy( "this" )
270        protected void changeConfiguration() {
271            this.configuration = null;
272        }
273    
274        /**
275         * Utility to load the current configuration for this source from the {@link RepositoryContext#getConfiguration(int)
276         * configuration repository}. This method may only be called after the source is {@link #initialize(RepositoryContext)
277         * initialized}.
278         * 
279         * @param name the name of the source; may not be null
280         * @param repositoryContext the repository context; may not be null
281         * @return the configuration; never null
282         * @throws RepositorySourceException if there is a problem with the configuration
283         */
284        protected FederatedRepository loadRepository( String name,
285                                                      RepositoryContext repositoryContext ) throws RepositorySourceException {
286            // All the required properties have been set ...
287            ExecutionContext executionContext = repositoryContext.getExecutionContext();
288            RepositoryConnectionFactory connectionFactory = repositoryContext.getRepositoryConnectionFactory();
289            ValueFactories valueFactories = executionContext.getValueFactories();
290            ValueFactory<String> strings = valueFactories.getStringFactory();
291            ValueFactory<Long> longs = valueFactories.getLongFactory();
292            ProjectionParser projectionParser = ProjectionParser.getInstance();
293            NamespaceRegistry registry = executionContext.getNamespaceRegistry();
294    
295            try {
296                // Read the configuration for the federated repository:
297                // Level 1: the node representing the federated repository
298                // Level 2: the "dna:workspaces" node
299                // Level 3: a node for each workspace in the federated repository
300                // Level 4: the "dna:projections" nodes
301                // Level 5: a node below "dna:projections" for each projection, with properties for the source name,
302                // workspace name, cache expiration time, and projection rules
303                Subgraph repositories = repositoryContext.getConfiguration(5);
304    
305                // Get the name of the default workspace ...
306                String defaultWorkspaceName = null;
307                Property defaultWorkspaceNameProperty = repositories.getRoot().getProperty(DnaLexicon.DEFAULT_WORKSPACE_NAME);
308                if (defaultWorkspaceNameProperty != null) {
309                    // Set the name using the property if there is one ...
310                    defaultWorkspaceName = strings.create(defaultWorkspaceNameProperty.getFirstValue());
311                }
312    
313                // Get the default expiration time for the repository ...
314                CachePolicy defaultCachePolicy = null;
315                Property timeToExpire = repositories.getRoot().getProperty(DnaLexicon.TIME_TO_EXPIRE);
316                if (timeToExpire != null && !timeToExpire.isEmpty()) {
317                    long timeToCacheInMillis = longs.create(timeToExpire.getFirstValue());
318                    defaultCachePolicy = new BasicCachePolicy(timeToCacheInMillis, TimeUnit.MILLISECONDS).getUnmodifiable();
319                }
320    
321                // Level 2: The "dna:workspaces" node ...
322                Node workspacesNode = repositories.getNode(DnaLexicon.WORKSPACES);
323                if (workspacesNode == null) {
324                    I18n msg = GraphI18n.requiredNodeDoesNotExistRelativeToNode;
325                    throw new RepositorySourceException(msg.text(DnaLexicon.WORKSPACES.getString(registry),
326                                                                 repositories.getLocation().getPath().getString(registry),
327                                                                 repositories.getGraph().getCurrentWorkspaceName(),
328                                                                 repositories.getGraph().getSourceName()));
329                }
330    
331                // Level 3: The workspace nodes ...
332                LinkedList<FederatedWorkspace> workspaces = new LinkedList<FederatedWorkspace>();
333                for (Location workspace : workspacesNode) {
334    
335                    // Get the name of the workspace ...
336                    String workspaceName = null;
337                    SubgraphNode workspaceNode = repositories.getNode(workspace);
338                    Property workspaceNameProperty = workspaceNode.getProperty(DnaLexicon.WORKSPACE_NAME);
339                    if (workspaceNameProperty != null) {
340                        // Set the name using the property if there is one ...
341                        workspaceName = strings.create(workspaceNameProperty.getFirstValue());
342                    }
343                    if (workspaceName == null) {
344                        // Otherwise, set the name using the local name of the workspace node ...
345                        workspaceName = workspace.getPath().getLastSegment().getName().getLocalName();
346                    }
347    
348                    // Level 4: the "dna:projections" node ...
349                    Node projectionsNode = workspaceNode.getNode(DnaLexicon.PROJECTIONS);
350                    if (projectionsNode == null) {
351                        I18n msg = GraphI18n.requiredNodeDoesNotExistRelativeToNode;
352                        throw new RepositorySourceException(getName(), msg.text(DnaLexicon.PROJECTIONS.getString(registry),
353                                                                                workspaceNode.getLocation()
354                                                                                             .getPath()
355                                                                                             .getString(registry),
356                                                                                repositories.getGraph().getCurrentWorkspaceName(),
357                                                                                repositories.getGraph().getSourceName()));
358                    }
359    
360                    // Level 5: the projection nodes ...
361                    List<Projection> sourceProjections = new LinkedList<Projection>();
362                    for (Location projection : projectionsNode) {
363                        Node projectionNode = repositories.getNode(projection);
364                        sourceProjections.add(createProjection(executionContext, projectionParser, projectionNode));
365                    }
366    
367                    // Create the federated workspace configuration ...
368                    FederatedWorkspace space = new FederatedWorkspace(repositoryContext, name, workspaceName, sourceProjections,
369                                                                      defaultCachePolicy);
370                    if (workspaceName.equals(defaultWorkspaceName)) {
371                        workspaces.addFirst(space);
372                    } else {
373                        workspaces.add(space);
374                    }
375                }
376    
377                // Create the ExecutorService ...
378                ExecutorService executor = Executors.newCachedThreadPool();
379    
380                return new FederatedRepository(name, connectionFactory, workspaces, defaultCachePolicy, executor);
381            } catch (RepositorySourceException t) {
382                throw t; // rethrow
383            } catch (Throwable t) {
384                I18n msg = GraphI18n.errorReadingConfigurationForFederatedRepositorySource;
385                throw new RepositorySourceException(getName(), msg.text(name), t);
386            }
387        }
388    
389        /**
390         * Instantiate the {@link Projection} described by the supplied properties.
391         * 
392         * @param context the execution context that should be used to read the configuration; may not be null
393         * @param projectionParser the projection rule parser that should be used; may not be null
394         * @param node the node where these properties were found; never null
395         * @return the region instance, or null if it could not be created
396         */
397        protected Projection createProjection( ExecutionContext context,
398                                               ProjectionParser projectionParser,
399                                               Node node ) {
400            ValueFactory<String> strings = context.getValueFactories().getStringFactory();
401    
402            Path path = node.getLocation().getPath();
403    
404            // Get the source name from the local name of the node ...
405            String sourceName = path.getLastSegment().getName().getLocalName();
406            Property sourceNameProperty = node.getProperty(DnaLexicon.SOURCE_NAME);
407            if (sourceNameProperty != null && !sourceNameProperty.isEmpty()) {
408                // There is a "dna:sourceName" property, so use this instead ...
409                sourceName = strings.create(sourceNameProperty.getFirstValue());
410            }
411            assert sourceName != null;
412    
413            // Get the workspace name ...
414            String workspaceName = null;
415            Property workspaceNameProperty = node.getProperty(DnaLexicon.WORKSPACE_NAME);
416            if (workspaceNameProperty != null && !workspaceNameProperty.isEmpty()) {
417                // There is a "dna:workspaceName" property, so use this instead ...
418                workspaceName = strings.create(workspaceNameProperty.getFirstValue());
419            }
420    
421            // Get the projection rules ...
422            Projection.Rule[] projectionRules = null;
423            Property projectionRulesProperty = node.getProperty(DnaLexicon.PROJECTION_RULES);
424            if (projectionRulesProperty != null && !projectionRulesProperty.isEmpty()) {
425                String[] projectionRuleStrs = strings.create(projectionRulesProperty.getValuesAsArray());
426                if (projectionRuleStrs != null && projectionRuleStrs.length != 0) {
427                    projectionRules = projectionParser.rulesFromStrings(context, projectionRuleStrs);
428                }
429            }
430    
431            // Is this projection read-only?
432            boolean readOnly = false;
433            Property readOnlyProperty = node.getProperty(DnaLexicon.READ_ONLY);
434            if (readOnlyProperty != null && !readOnlyProperty.isEmpty()) {
435                readOnly = context.getValueFactories().getBooleanFactory().create(readOnlyProperty.getFirstValue());
436            }
437    
438            return new Projection(sourceName, workspaceName, readOnly, projectionRules);
439        }
440    
441    }