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