View Javadoc

1   /*
2    * ModeShape (http://www.modeshape.org)
3    * See the COPYRIGHT.txt file distributed with this work for information
4    * regarding copyright ownership.  Some portions may be licensed
5    * to Red Hat, Inc. under one or more contributor license agreements.
6    * See the AUTHORS.txt file in the distribution for a full listing of 
7    * individual contributors.
8    *
9    * Unless otherwise indicated, all code in ModeShape is licensed
10   * to you under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation; either version 2.1 of
12   * the License, or (at your option) any later version.
13   * 
14   * ModeShape is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17   * Lesser General Public License for more details.
18   *
19   * You should have received a copy of the GNU Lesser General Public
20   * License along with this software; if not, write to the Free
21   * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
22   * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
23   */
24  package org.modeshape.graph.connector.federation;
25  
26  import java.util.Enumeration;
27  import java.util.HashMap;
28  import java.util.Hashtable;
29  import java.util.LinkedList;
30  import java.util.List;
31  import java.util.Map;
32  import java.util.concurrent.ExecutorService;
33  import java.util.concurrent.Executors;
34  import java.util.concurrent.TimeUnit;
35  import javax.naming.Context;
36  import javax.naming.Name;
37  import javax.naming.RefAddr;
38  import javax.naming.Reference;
39  import javax.naming.StringRefAddr;
40  import javax.naming.spi.ObjectFactory;
41  import net.jcip.annotations.GuardedBy;
42  import net.jcip.annotations.ThreadSafe;
43  import org.modeshape.common.i18n.I18n;
44  import org.modeshape.common.util.CheckArg;
45  import org.modeshape.common.util.HashCode;
46  import org.modeshape.common.util.NamedThreadFactory;
47  import org.modeshape.graph.ExecutionContext;
48  import org.modeshape.graph.GraphI18n;
49  import org.modeshape.graph.Location;
50  import org.modeshape.graph.ModeShapeLexicon;
51  import org.modeshape.graph.Node;
52  import org.modeshape.graph.Subgraph;
53  import org.modeshape.graph.SubgraphNode;
54  import org.modeshape.graph.cache.BasicCachePolicy;
55  import org.modeshape.graph.cache.CachePolicy;
56  import org.modeshape.graph.connector.RepositoryConnection;
57  import org.modeshape.graph.connector.RepositoryConnectionFactory;
58  import org.modeshape.graph.connector.RepositoryContext;
59  import org.modeshape.graph.connector.RepositorySource;
60  import org.modeshape.graph.connector.RepositorySourceCapabilities;
61  import org.modeshape.graph.connector.RepositorySourceException;
62  import org.modeshape.graph.observe.Observer;
63  import org.modeshape.graph.property.NamespaceRegistry;
64  import org.modeshape.graph.property.Path;
65  import org.modeshape.graph.property.Property;
66  import org.modeshape.graph.property.ValueFactories;
67  import org.modeshape.graph.property.ValueFactory;
68  
69  /**
70   * A {@link RepositorySource} for a federated repository.
71   */
72  @ThreadSafe
73  public class FederatedRepositorySource implements RepositorySource, ObjectFactory {
74  
75      /**
76       * The default limit is {@value} for retrying {@link RepositoryConnection connection} calls to the underlying source.
77       */
78      public static final int DEFAULT_RETRY_LIMIT = 0;
79  
80      protected static final String SOURCE_NAME = "sourceName";
81      protected static final String RETRY_LIMIT = "retryLimit";
82  
83      private static final long serialVersionUID = 1L;
84  
85      private volatile String name;
86      private volatile int retryLimit;
87      private volatile RepositorySourceCapabilities capabilities = new RepositorySourceCapabilities(true, true, false, false, true);
88      private volatile transient FederatedRepository configuration;
89      private volatile transient RepositoryContext context;
90  
91      /**
92       * Construct a new instance of a {@link RepositorySource} for a federated repository.
93       */
94      public FederatedRepositorySource() {
95      }
96  
97      /**
98       * {@inheritDoc}
99       * 
100      * @see org.modeshape.graph.connector.RepositorySource#getName()
101      */
102     public String getName() {
103         return name;
104     }
105 
106     /**
107      * @param name Sets name to the specified value.
108      */
109     public synchronized void setName( String name ) {
110         if (this.name == name || this.name != null && this.name.equals(name)) return; // unchanged
111         this.name = name;
112         changeConfiguration();
113     }
114 
115     /**
116      * {@inheritDoc}
117      * 
118      * @see org.modeshape.graph.connector.RepositorySource#getRetryLimit()
119      */
120     public int getRetryLimit() {
121         return retryLimit;
122     }
123 
124     /**
125      * {@inheritDoc}
126      * 
127      * @see org.modeshape.graph.connector.RepositorySource#setRetryLimit(int)
128      */
129     public synchronized void setRetryLimit( int limit ) {
130         retryLimit = limit < 0 ? 0 : limit;
131         changeConfiguration();
132     }
133 
134     /**
135      * {@inheritDoc}
136      * 
137      * @see org.modeshape.graph.connector.RepositorySource#getCapabilities()
138      */
139     public RepositorySourceCapabilities getCapabilities() {
140         return capabilities;
141     }
142 
143     /**
144      * {@inheritDoc}
145      * 
146      * @see org.modeshape.graph.connector.RepositorySource#initialize(org.modeshape.graph.connector.RepositoryContext)
147      */
148     public synchronized void initialize( RepositoryContext context ) throws RepositorySourceException {
149         this.context = context;
150         changeConfiguration();
151     }
152 
153     /**
154      * Get the repository context that was used to {@link #initialize(RepositoryContext) initialize} this source.
155      * 
156      * @return the context, or null if the source was not yet {@link #initialize(RepositoryContext) initialized}
157      */
158     /*package*/RepositoryContext getRepositoryContext() {
159         return context;
160     }
161 
162     /**
163      * {@inheritDoc}
164      * 
165      * @see org.modeshape.graph.connector.RepositorySource#getConnection()
166      */
167     public RepositoryConnection getConnection() throws RepositorySourceException {
168         FederatedRepository config = this.configuration;
169         if (config == null) {
170             synchronized (this) {
171                 if (this.configuration == null) {
172                     // Check all the properties of this source ...
173                     String name = getName();
174                     if (name == null) {
175                         I18n msg = GraphI18n.namePropertyIsRequiredForFederatedRepositorySource;
176                         throw new RepositorySourceException(getName(), msg.text("name"));
177                     }
178                     RepositoryContext repositoryContext = getRepositoryContext();
179                     if (repositoryContext == null) {
180                         I18n msg = GraphI18n.federatedRepositorySourceMustBeInitialized;
181                         throw new RepositorySourceException(getName(), msg.text("name", name));
182                     }
183 
184                     // Load the configuration ...
185                     this.configuration = loadRepository(name, repositoryContext);
186                 }
187                 config = this.configuration;
188             }
189         }
190         Observer observer = this.context != null ? this.context.getObserver() : null;
191         return new FederatedRepositoryConnection(config, observer);
192     }
193 
194     /**
195      * {@inheritDoc}
196      * 
197      * @see org.modeshape.graph.connector.RepositorySource#close()
198      */
199     public void close() {
200         synchronized (this) {
201             if (this.configuration != null) {
202                 // Release the configuration ...
203                 if (this.configuration.getExecutor() != null) {
204                     this.configuration.getExecutor().shutdown();
205                 }
206                 this.configuration = null;
207             }
208         }
209     }
210 
211     /**
212      * {@inheritDoc}
213      * 
214      * @see javax.naming.Referenceable#getReference()
215      */
216     public Reference getReference() {
217         String className = getClass().getName();
218         String factoryClassName = this.getClass().getName();
219         Reference ref = new Reference(className, factoryClassName, null);
220 
221         ref.add(new StringRefAddr(SOURCE_NAME, getName()));
222         ref.add(new StringRefAddr(RETRY_LIMIT, Integer.toString(getRetryLimit())));
223         return ref;
224     }
225 
226     /**
227      * {@inheritDoc}
228      * 
229      * @see javax.naming.spi.ObjectFactory#getObjectInstance(java.lang.Object, javax.naming.Name, javax.naming.Context,
230      *      java.util.Hashtable)
231      */
232     public Object getObjectInstance( Object obj,
233                                      Name name,
234                                      Context nameCtx,
235                                      Hashtable<?, ?> environment ) throws Exception {
236         if (obj instanceof Reference) {
237             Map<String, String> values = new HashMap<String, String>();
238             Reference ref = (Reference)obj;
239             Enumeration<?> en = ref.getAll();
240             while (en.hasMoreElements()) {
241                 RefAddr subref = (RefAddr)en.nextElement();
242                 if (subref instanceof StringRefAddr) {
243                     String key = subref.getType();
244                     Object value = subref.getContent();
245                     if (value != null) values.put(key, value.toString());
246                 }
247             }
248             String sourceName = values.get(SOURCE_NAME);
249             String retryLimit = values.get(RETRY_LIMIT);
250 
251             // Create the source instance ...
252             FederatedRepositorySource source = new FederatedRepositorySource();
253             if (sourceName != null) source.setName(sourceName);
254             if (retryLimit != null) source.setRetryLimit(Integer.parseInt(retryLimit));
255             return source;
256         }
257         return null;
258     }
259 
260     /**
261      * {@inheritDoc}
262      */
263     @Override
264     public int hashCode() {
265         return HashCode.compute(getName());
266     }
267 
268     /**
269      * {@inheritDoc}
270      */
271     @Override
272     public boolean equals( Object obj ) {
273         if (obj == this) return true;
274         if (obj instanceof FederatedRepositorySource) {
275             FederatedRepositorySource that = (FederatedRepositorySource)obj;
276             // The source name must match
277             if (this.getName() == null) {
278                 if (that.getName() != null) return false;
279             } else {
280                 if (!this.getName().equals(that.getName())) return false;
281             }
282             return true;
283         }
284         return false;
285     }
286 
287     /**
288      * Mark the current configuration (if there is one) as being invalid.
289      */
290     @GuardedBy( "this" )
291     protected void changeConfiguration() {
292         this.configuration = null;
293     }
294 
295     /**
296      * Utility to load the current configuration for this source from the {@link RepositoryContext#getConfiguration(int)
297      * configuration repository}. This method may only be called after the source is {@link #initialize(RepositoryContext)
298      * initialized}.
299      * 
300      * @param name the name of the source; may not be null
301      * @param repositoryContext the repository context; may not be null
302      * @return the configuration; never null
303      * @throws RepositorySourceException if there is a problem with the configuration
304      */
305     protected FederatedRepository loadRepository( String name,
306                                                   RepositoryContext repositoryContext ) throws RepositorySourceException {
307         // All the required properties have been set ...
308         ExecutionContext executionContext = repositoryContext.getExecutionContext();
309         RepositoryConnectionFactory connectionFactory = repositoryContext.getRepositoryConnectionFactory();
310         ValueFactories valueFactories = executionContext.getValueFactories();
311         ValueFactory<String> strings = valueFactories.getStringFactory();
312         ValueFactory<Long> longs = valueFactories.getLongFactory();
313         ProjectionParser projectionParser = ProjectionParser.getInstance();
314         NamespaceRegistry registry = executionContext.getNamespaceRegistry();
315 
316         try {
317             // Read the configuration for the federated repository:
318             // Level 1: the node representing the federated repository
319             // Level 2: the "dna:workspaces" node
320             // Level 3: a node for each workspace in the federated repository
321             // Level 4: the "dna:projections" nodes
322             // Level 5: a node below "dna:projections" for each projection, with properties for the source name,
323             // workspace name, cache expiration time, and projection rules
324             Subgraph repositories = repositoryContext.getConfiguration(5);
325 
326             // Get the name of the default workspace ...
327             String defaultWorkspaceName = null;
328             Property defaultWorkspaceNameProperty = repositories.getRoot().getProperty(ModeShapeLexicon.DEFAULT_WORKSPACE_NAME);
329             if (defaultWorkspaceNameProperty != null) {
330                 // Set the name using the property if there is one ...
331                 defaultWorkspaceName = strings.create(defaultWorkspaceNameProperty.getFirstValue());
332             }
333 
334             // Get the default expiration time for the repository ...
335             CachePolicy defaultCachePolicy = null;
336             Property timeToExpire = repositories.getRoot().getProperty(ModeShapeLexicon.TIME_TO_EXPIRE);
337             if (timeToExpire != null && !timeToExpire.isEmpty()) {
338                 long timeToCacheInMillis = longs.create(timeToExpire.getFirstValue());
339                 defaultCachePolicy = new BasicCachePolicy(timeToCacheInMillis, TimeUnit.MILLISECONDS).getUnmodifiable();
340             }
341 
342             // Level 2: The "dna:workspaces" node ...
343             Node workspacesNode = repositories.getNode(ModeShapeLexicon.WORKSPACES);
344             if (workspacesNode == null) {
345                 I18n msg = GraphI18n.requiredNodeDoesNotExistRelativeToNode;
346                 throw new RepositorySourceException(msg.text(ModeShapeLexicon.WORKSPACES.getString(registry),
347                                                              repositories.getLocation().getPath().getString(registry),
348                                                              repositories.getGraph().getCurrentWorkspaceName(),
349                                                              repositories.getGraph().getSourceName()));
350             }
351 
352             // Level 3: The workspace nodes ...
353             LinkedList<FederatedWorkspace> workspaces = new LinkedList<FederatedWorkspace>();
354             for (Location workspace : workspacesNode) {
355 
356                 // Get the name of the workspace ...
357                 String workspaceName = null;
358                 SubgraphNode workspaceNode = repositories.getNode(workspace);
359                 Property workspaceNameProperty = workspaceNode.getProperty(ModeShapeLexicon.WORKSPACE_NAME);
360                 if (workspaceNameProperty != null) {
361                     // Set the name using the property if there is one ...
362                     workspaceName = strings.create(workspaceNameProperty.getFirstValue());
363                 }
364                 if (workspaceName == null) {
365                     // Otherwise, set the name using the local name of the workspace node ...
366                     workspaceName = workspace.getPath().getLastSegment().getName().getLocalName();
367                 }
368 
369                 // Level 4: the "dna:projections" node ...
370                 Node projectionsNode = workspaceNode.getNode(ModeShapeLexicon.PROJECTIONS);
371                 if (projectionsNode == null) {
372                     I18n msg = GraphI18n.requiredNodeDoesNotExistRelativeToNode;
373                     throw new RepositorySourceException(getName(), msg.text(ModeShapeLexicon.PROJECTIONS.getString(registry),
374                                                                             workspaceNode.getLocation()
375                                                                                          .getPath()
376                                                                                          .getString(registry),
377                                                                             repositories.getGraph().getCurrentWorkspaceName(),
378                                                                             repositories.getGraph().getSourceName()));
379                 }
380 
381                 // Level 5: the projection nodes ...
382                 List<Projection> sourceProjections = new LinkedList<Projection>();
383                 for (Location projection : projectionsNode) {
384                     Node projectionNode = repositories.getNode(projection);
385                     sourceProjections.add(createProjection(executionContext, projectionParser, projectionNode));
386                 }
387 
388                 // Create the federated workspace configuration ...
389                 FederatedWorkspace space = new FederatedWorkspace(repositoryContext, name, workspaceName, sourceProjections,
390                                                                   defaultCachePolicy);
391                 if (workspaceName.equals(defaultWorkspaceName)) {
392                     workspaces.addFirst(space);
393                 } else {
394                     workspaces.add(space);
395                 }
396             }
397 
398             // Create the ExecutorService ...
399             ExecutorService executor = Executors.newCachedThreadPool(new NamedThreadFactory(name));
400 
401             return new FederatedRepository(name, connectionFactory, workspaces, defaultCachePolicy, executor);
402         } catch (RepositorySourceException t) {
403             throw t; // rethrow
404         } catch (Throwable t) {
405             I18n msg = GraphI18n.errorReadingConfigurationForFederatedRepositorySource;
406             throw new RepositorySourceException(getName(), msg.text(name), t);
407         }
408     }
409 
410     /**
411      * Add a federated workspace to this source. If a workspace with the supplied name already exists, it will be replaced with
412      * the new one.
413      * 
414      * @param workspaceName the name of the new federated workspace
415      * @param projections the projections that should be used in the workspace
416      * @param isDefault true if this workspace should be used as the default workspace, or false otherwise
417      * @return the federated workspace
418      * @throws IllegalArgumentException if the workspace name or the projections reference are null
419      */
420     public synchronized FederatedWorkspace addWorkspace( String workspaceName,
421                                                          Iterable<Projection> projections,
422                                                          boolean isDefault ) {
423         CheckArg.isNotNull(workspaceName, "workspaceName");
424         CheckArg.isNotNull(projections, "projections");
425 
426         // Check all the properties of this source ...
427         String name = getName();
428         if (name == null) {
429             I18n msg = GraphI18n.namePropertyIsRequiredForFederatedRepositorySource;
430             throw new RepositorySourceException(getName(), msg.text("name"));
431         }
432         RepositoryContext context = getRepositoryContext();
433         if (context == null) {
434             I18n msg = GraphI18n.federatedRepositorySourceMustBeInitialized;
435             throw new RepositorySourceException(getName(), msg.text("name", name));
436         }
437 
438         // Now set up or get the existing components needed by the workspace ...
439         RepositoryConnectionFactory connectionFactory = null;
440         ExecutorService executor = null;
441         LinkedList<FederatedWorkspace> workspaces = new LinkedList<FederatedWorkspace>();
442         CachePolicy defaultCachePolicy = null;
443         if (this.configuration != null) {
444             connectionFactory = this.configuration.getConnectionFactory();
445             executor = this.configuration.getExecutor();
446             defaultCachePolicy = this.configuration.getDefaultCachePolicy();
447             for (String existingWorkspaceName : this.configuration.getWorkspaceNames()) {
448                 if (existingWorkspaceName.equals(workspaceName)) continue;
449                 workspaces.add(this.configuration.getWorkspace(existingWorkspaceName));
450             }
451         } else {
452             connectionFactory = context.getRepositoryConnectionFactory();
453             executor = Executors.newCachedThreadPool(new NamedThreadFactory(name));
454         }
455 
456         // Add the new workspace ...
457         FederatedWorkspace newWorkspace = new FederatedWorkspace(context, name, workspaceName, projections, defaultCachePolicy);
458         if (isDefault) {
459             workspaces.addFirst(newWorkspace);
460         } else {
461             workspaces.add(newWorkspace);
462         }
463         // Update the configuration ...
464         this.configuration = new FederatedRepository(name, connectionFactory, workspaces, defaultCachePolicy, executor);
465         return newWorkspace;
466     }
467 
468     /**
469      * Remove the named workspace from the repository source.
470      * 
471      * @param workspaceName the name of the workspace to remove
472      * @return true if the workspace was removed, or false otherwise
473      * @throws IllegalArgumentException if the workspace name is null
474      */
475     public synchronized boolean removeWorkspace( String workspaceName ) {
476         CheckArg.isNotNull(workspaceName, "workspaceName");
477         if (this.configuration == null) return false;
478         FederatedWorkspace workspace = this.configuration.getWorkspace(workspaceName);
479         if (workspace == null) return false;
480         List<FederatedWorkspace> workspaces = new LinkedList<FederatedWorkspace>();
481         for (String existingWorkspaceName : this.configuration.getWorkspaceNames()) {
482             if (existingWorkspaceName.equals(workspaceName)) continue;
483             workspaces.add(this.configuration.getWorkspace(existingWorkspaceName));
484         }
485         RepositoryConnectionFactory connectionFactory = this.configuration.getConnectionFactory();
486         ExecutorService executor = this.configuration.getExecutor();
487         CachePolicy defaultCachePolicy = this.configuration.getDefaultCachePolicy();
488         this.configuration = new FederatedRepository(name, connectionFactory, workspaces, defaultCachePolicy, executor);
489         return true;
490     }
491 
492     public synchronized boolean hasWorkspace( String workspaceName ) {
493         CheckArg.isNotNull(workspaceName, "workspaceName");
494         return this.configuration != null && this.configuration.getWorkspaceNames().contains(workspaceName);
495     }
496 
497     /**
498      * Instantiate the {@link Projection} described by the supplied properties.
499      * 
500      * @param context the execution context that should be used to read the configuration; may not be null
501      * @param projectionParser the projection rule parser that should be used; may not be null
502      * @param node the node where these properties were found; never null
503      * @return the region instance, or null if it could not be created
504      */
505     protected Projection createProjection( ExecutionContext context,
506                                            ProjectionParser projectionParser,
507                                            Node node ) {
508         ValueFactory<String> strings = context.getValueFactories().getStringFactory();
509 
510         Path path = node.getLocation().getPath();
511 
512         // Get the source name from the local name of the node ...
513         String sourceName = path.getLastSegment().getName().getLocalName();
514         Property sourceNameProperty = node.getProperty(ModeShapeLexicon.SOURCE_NAME);
515         if (sourceNameProperty != null && !sourceNameProperty.isEmpty()) {
516             // There is a "dna:sourceName" property, so use this instead ...
517             sourceName = strings.create(sourceNameProperty.getFirstValue());
518         }
519         assert sourceName != null;
520 
521         // Get the workspace name ...
522         String workspaceName = null;
523         Property workspaceNameProperty = node.getProperty(ModeShapeLexicon.WORKSPACE_NAME);
524         if (workspaceNameProperty != null && !workspaceNameProperty.isEmpty()) {
525             // There is a "dna:workspaceName" property, so use this instead ...
526             workspaceName = strings.create(workspaceNameProperty.getFirstValue());
527         }
528 
529         // Get the projection rules ...
530         Projection.Rule[] projectionRules = null;
531         Property projectionRulesProperty = node.getProperty(ModeShapeLexicon.PROJECTION_RULES);
532         if (projectionRulesProperty != null && !projectionRulesProperty.isEmpty()) {
533             String[] projectionRuleStrs = strings.create(projectionRulesProperty.getValuesAsArray());
534             if (projectionRuleStrs != null && projectionRuleStrs.length != 0) {
535                 projectionRules = projectionParser.rulesFromStrings(context, projectionRuleStrs);
536             }
537         }
538 
539         // Is this projection read-only?
540         boolean readOnly = false;
541         Property readOnlyProperty = node.getProperty(ModeShapeLexicon.READ_ONLY);
542         if (readOnlyProperty != null && !readOnlyProperty.isEmpty()) {
543             readOnly = context.getValueFactories().getBooleanFactory().create(readOnlyProperty.getFirstValue());
544         }
545 
546         return new Projection(sourceName, workspaceName, readOnly, projectionRules);
547     }
548 
549 }