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    * ModeShape is free software. Unless otherwise indicated, all code in ModeShape
10   * is licensed 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.inmemory;
25  
26  import java.io.ByteArrayInputStream;
27  import java.io.ByteArrayOutputStream;
28  import java.io.IOException;
29  import java.io.ObjectInputStream;
30  import java.io.ObjectOutputStream;
31  import java.util.Enumeration;
32  import java.util.HashMap;
33  import java.util.Hashtable;
34  import java.util.List;
35  import java.util.Map;
36  import java.util.UUID;
37  import java.util.concurrent.atomic.AtomicInteger;
38  import javax.naming.BinaryRefAddr;
39  import javax.naming.Context;
40  import javax.naming.InitialContext;
41  import javax.naming.NamingException;
42  import javax.naming.RefAddr;
43  import javax.naming.Reference;
44  import javax.naming.StringRefAddr;
45  import javax.naming.spi.ObjectFactory;
46  import net.jcip.annotations.GuardedBy;
47  import net.jcip.annotations.ThreadSafe;
48  import org.modeshape.common.i18n.I18n;
49  import org.modeshape.common.util.CheckArg;
50  import org.modeshape.common.util.StringUtil;
51  import org.modeshape.graph.ExecutionContext;
52  import org.modeshape.graph.GraphI18n;
53  import org.modeshape.graph.Subgraph;
54  import org.modeshape.graph.cache.CachePolicy;
55  import org.modeshape.graph.connector.RepositoryConnection;
56  import org.modeshape.graph.connector.RepositoryConnectionFactory;
57  import org.modeshape.graph.connector.RepositoryContext;
58  import org.modeshape.graph.connector.RepositorySource;
59  import org.modeshape.graph.connector.RepositorySourceCapabilities;
60  import org.modeshape.graph.connector.RepositorySourceException;
61  import org.modeshape.graph.connector.base.BaseRepositorySource;
62  import org.modeshape.graph.connector.base.Connection;
63  import org.modeshape.graph.observe.Observer;
64  import org.modeshape.graph.request.CreateWorkspaceRequest.CreateConflictBehavior;
65  
66  /**
67   * A {@link RepositorySource} for an in-memory repository. Each {@link InMemoryRepositorySource} instance contains its own
68   * repository, and the lifetime of the source dictates the lifetime of the repository and its content.
69   */
70  @ThreadSafe
71  public class InMemoryRepositorySource implements BaseRepositorySource, ObjectFactory {
72  
73      /**
74       * The initial version is 1
75       */
76      private static final long serialVersionUID = 1L;
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      /**
84       * The default name for the workspace used by this source, which is a blank string.
85       */
86      public static final String DEFAULT_WORKSPACE_NAME = "";
87  
88      protected static final RepositorySourceCapabilities CAPABILITIES = new RepositorySourceCapabilities(true, true, false, true,
89                                                                                                          true);
90  
91      protected static final String ROOT_NODE_UUID_ATTR = "rootNodeUuid";
92      protected static final String SOURCE_NAME_ATTR = "sourceName";
93      protected static final String PREDEFINED_WORKSPACE_NAMES = "predefinedWorkspaceNames";
94      protected static final String DEFAULT_WORKSPACE_NAME_ATTR = "defaultWorkspaceName";
95      protected static final String DEFAULT_CACHE_POLICY_ATTR = "defaultCachePolicy";
96      protected static final String JNDI_NAME_ATTR = "jndiName";
97      protected static final String RETRY_LIMIT_ATTR = "retryLimit";
98  
99      @GuardedBy( "sourcesLock" )
100     private String name;
101     @GuardedBy( "this" )
102     private String jndiName;
103     private String defaultWorkspaceName = DEFAULT_WORKSPACE_NAME;
104     private UUID rootNodeUuid = UUID.randomUUID();
105     private CachePolicy defaultCachePolicy;
106     private volatile String[] predefinedWorkspaces = new String[] {};
107     private final AtomicInteger retryLimit = new AtomicInteger(DEFAULT_RETRY_LIMIT);
108     private transient InMemoryRepository repository;
109     private transient ExecutionContext defaultContext = new ExecutionContext();
110     private transient RepositoryContext repositoryContext = new DefaultRepositoryContext();
111 
112     protected class DefaultRepositoryContext implements RepositoryContext {
113         /**
114          * {@inheritDoc}
115          * 
116          * @see org.modeshape.graph.connector.RepositoryContext#getExecutionContext()
117          */
118         @SuppressWarnings( "synthetic-access" )
119         public ExecutionContext getExecutionContext() {
120             return defaultContext;
121         }
122 
123         /**
124          * {@inheritDoc}
125          * 
126          * @see org.modeshape.graph.connector.RepositoryContext#getConfiguration(int)
127          */
128         public Subgraph getConfiguration( int depth ) {
129             return null;
130         }
131 
132         /**
133          * {@inheritDoc}
134          * 
135          * @see org.modeshape.graph.connector.RepositoryContext#getObserver()
136          */
137         public Observer getObserver() {
138             return null;
139         }
140 
141         /**
142          * {@inheritDoc}
143          * 
144          * @see org.modeshape.graph.connector.RepositoryContext#getRepositoryConnectionFactory()
145          */
146         public RepositoryConnectionFactory getRepositoryConnectionFactory() {
147             return null;
148         }
149     }
150 
151     /**
152      * Create a repository source instance.
153      */
154     public InMemoryRepositorySource() {
155         super();
156     }
157 
158     /**
159      * {@inheritDoc}
160      * 
161      * @see org.modeshape.graph.connector.RepositorySource#initialize(org.modeshape.graph.connector.RepositoryContext)
162      */
163     public void initialize( RepositoryContext context ) throws RepositorySourceException {
164         this.repositoryContext = context != null ? context : new DefaultRepositoryContext();
165     }
166 
167     /**
168      * @return repositoryContext
169      */
170     public RepositoryContext getRepositoryContext() {
171         return repositoryContext;
172     }
173 
174     /**
175      * {@inheritDoc}
176      * 
177      * @see org.modeshape.graph.connector.RepositorySource#getRetryLimit()
178      */
179     public int getRetryLimit() {
180         return retryLimit.get();
181     }
182 
183     /**
184      * {@inheritDoc}
185      * 
186      * @see org.modeshape.graph.connector.RepositorySource#setRetryLimit(int)
187      */
188     public void setRetryLimit( int limit ) {
189         retryLimit.set(limit < 0 ? 0 : limit);
190     }
191 
192     /**
193      * Get the default cache policy for this source, or null if the global default cache policy should be used
194      * 
195      * @return the default cache policy, or null if this source has no explicit default cache policy
196      */
197     public CachePolicy getDefaultCachePolicy() {
198         return defaultCachePolicy;
199     }
200 
201     /**
202      * @param defaultCachePolicy Sets defaultCachePolicy to the specified value.
203      */
204     public void setDefaultCachePolicy( CachePolicy defaultCachePolicy ) {
205         this.defaultCachePolicy = defaultCachePolicy;
206     }
207 
208     /**
209      * Get the name of the workspace that should be used by default.
210      * 
211      * @return the name of the default workspace
212      */
213     public String getDefaultWorkspaceName() {
214         return defaultWorkspaceName;
215     }
216 
217     /**
218      * Set the default workspace name.
219      * 
220      * @param defaultWorkspaceName the name of the workspace that should be used by default, or null if "" should be used
221      */
222     public void setDefaultWorkspaceName( String defaultWorkspaceName ) {
223         this.defaultWorkspaceName = defaultWorkspaceName != null ? defaultWorkspaceName : DEFAULT_WORKSPACE_NAME;
224     }
225 
226     /**
227      * Get the string representation of the UUID for the root node.
228      * 
229      * @return the root node's UUID as a string; may not be null
230      */
231     public String getRootNodeUuid() {
232         return this.rootNodeUuid.toString();
233     }
234 
235     /**
236      * {@inheritDoc}
237      * 
238      * @see org.modeshape.graph.connector.base.BaseRepositorySource#getRootNodeUuidObject()
239      */
240     public UUID getRootNodeUuidObject() {
241         return this.rootNodeUuid;
242     }
243 
244     /**
245      * @param rootNodeUuid Sets rootNodeUuid to the specified value.
246      */
247     public void setRootNodeUuid( UUID rootNodeUuid ) {
248         this.rootNodeUuid = rootNodeUuid != null ? rootNodeUuid : UUID.randomUUID();
249     }
250 
251     /**
252      * @param rootNodeUuid Sets rootNodeUuid to the specified value.
253      */
254     public void setRootNodeUuid( String rootNodeUuid ) {
255         this.rootNodeUuid = rootNodeUuid != null ? UUID.fromString(rootNodeUuid) : UUID.randomUUID();
256     }
257 
258     /**
259      * If you use this to set a JNDI name, this source will be bound to that name using the default {@link InitialContext}. You
260      * can also do this manually if you have additional requirements.
261      * 
262      * @param name the JNDI name
263      * @throws NamingException if there is a problem registering this object
264      * @see #getJndiName()
265      */
266     public void setJndiName( String name ) throws NamingException {
267         setJndiName(name, null);
268     }
269 
270     /**
271      * Register this source in JNDI under the supplied name using the supplied context. to set a JNDI name, this source will be
272      * bound to that name using the default {@link InitialContext}. You can also do this manually if you have additional
273      * requirements.
274      * 
275      * @param name the JNDI name, or null if this object is to no longer be registered
276      * @param context the JNDI context, or null if the {@link InitialContext} should be used
277      * @throws NamingException if there is a problem registering this object
278      * @see #getJndiName()
279      */
280     public synchronized void setJndiName( String name,
281                                           Context context ) throws NamingException {
282         CheckArg.isNotNull(name, "name");
283         if (context == null) context = new InitialContext();
284 
285         // First register in JNDI under the new name ...
286         if (name != null) {
287             context.bind(name, this);
288         }
289         // Second, unregister from JNDI if there is already a name ...
290         if (jndiName != null && !jndiName.equals(name)) {
291             context.unbind(jndiName);
292         }
293         // Record the new name ...
294         this.jndiName = name;
295     }
296 
297     /**
298      * Gets the JNDI name this source is bound to. Only valid if you used setJNDIName to bind it.
299      * 
300      * @return the JNDI name, or null if it is not bound in JNDI
301      * @see #setJndiName(String)
302      */
303     public synchronized String getJndiName() {
304         return jndiName;
305     }
306 
307     /**
308      * {@inheritDoc}
309      */
310     public String getName() {
311         return this.name;
312     }
313 
314     /**
315      * @param name Sets name to the specified value.
316      */
317     public void setName( String name ) {
318         this.name = name;
319     }
320 
321     /**
322      * {@inheritDoc}
323      * 
324      * @see org.modeshape.graph.connector.RepositorySource#getConnection()
325      */
326     public synchronized RepositoryConnection getConnection() throws RepositorySourceException {
327         if (repository == null) {
328             repository = new InMemoryRepository(this);
329 
330             ExecutionContext context = repositoryContext != null ? repositoryContext.getExecutionContext() : defaultContext;
331             InMemoryTransaction txn = repository.startTransaction(context, false);
332             try {
333                 // Create the set of initial workspaces ...
334                 for (String initialName : getPredefinedWorkspaceNames()) {
335                     repository.createWorkspace(txn, initialName, CreateConflictBehavior.DO_NOT_CREATE, null);
336                 }
337             } finally {
338                 txn.commit();
339             }
340 
341         }
342         return new Connection<InMemoryNode, InMemoryWorkspace>(this, repository);
343     }
344 
345     /**
346      * {@inheritDoc}
347      * 
348      * @see org.modeshape.graph.connector.RepositorySource#close()
349      */
350     public synchronized void close() {
351         // Null the reference to the in-memory repository; open connections still reference it and can continue to work ...
352         this.repository = null;
353     }
354 
355     /**
356      * {@inheritDoc}
357      */
358     public synchronized Reference getReference() {
359         String className = getClass().getName();
360         String factoryClassName = this.getClass().getName();
361         Reference ref = new Reference(className, factoryClassName, null);
362 
363         if (getName() != null) {
364             ref.add(new StringRefAddr(SOURCE_NAME_ATTR, getName()));
365         }
366         if (getRootNodeUuid() != null) {
367             ref.add(new StringRefAddr(ROOT_NODE_UUID_ATTR, getRootNodeUuid().toString()));
368         }
369         if (getJndiName() != null) {
370             ref.add(new StringRefAddr(JNDI_NAME_ATTR, getJndiName()));
371         }
372         if (getDefaultWorkspaceName() != null) {
373             ref.add(new StringRefAddr(DEFAULT_WORKSPACE_NAME_ATTR, getDefaultWorkspaceName()));
374         }
375         String[] workspaceNames = getPredefinedWorkspaceNames();
376         if (workspaceNames != null && workspaceNames.length != 0) {
377             ref.add(new StringRefAddr(PREDEFINED_WORKSPACE_NAMES, StringUtil.combineLines(workspaceNames)));
378         }
379         if (getDefaultCachePolicy() != null) {
380             ByteArrayOutputStream baos = new ByteArrayOutputStream();
381             CachePolicy policy = getDefaultCachePolicy();
382             try {
383                 ObjectOutputStream oos = new ObjectOutputStream(baos);
384                 oos.writeObject(policy);
385                 ref.add(new BinaryRefAddr(DEFAULT_CACHE_POLICY_ATTR, baos.toByteArray()));
386             } catch (IOException e) {
387                 I18n msg = GraphI18n.errorSerializingInMemoryCachePolicyInSource;
388                 throw new RepositorySourceException(getName(), msg.text(policy.getClass().getName(), getName()), e);
389             }
390         }
391         ref.add(new StringRefAddr(RETRY_LIMIT_ATTR, Integer.toString(getRetryLimit())));
392         return ref;
393     }
394 
395     /**
396      * {@inheritDoc}
397      */
398     public Object getObjectInstance( Object obj,
399                                      javax.naming.Name name,
400                                      Context nameCtx,
401                                      Hashtable<?, ?> environment ) throws Exception {
402         if (obj instanceof Reference) {
403             Map<String, Object> values = new HashMap<String, Object>();
404             Reference ref = (Reference)obj;
405             Enumeration<?> en = ref.getAll();
406             while (en.hasMoreElements()) {
407                 RefAddr subref = (RefAddr)en.nextElement();
408                 if (subref instanceof StringRefAddr) {
409                     String key = subref.getType();
410                     Object value = subref.getContent();
411                     if (value != null) values.put(key, value.toString());
412                 } else if (subref instanceof BinaryRefAddr) {
413                     String key = subref.getType();
414                     Object value = subref.getContent();
415                     if (value instanceof byte[]) {
416                         // Deserialize ...
417                         ByteArrayInputStream bais = new ByteArrayInputStream((byte[])value);
418                         ObjectInputStream ois = new ObjectInputStream(bais);
419                         value = ois.readObject();
420                         values.put(key, value);
421                     }
422                 }
423             }
424             String sourceName = (String)values.get(SOURCE_NAME_ATTR);
425             String rootNodeUuidString = (String)values.get(ROOT_NODE_UUID_ATTR);
426             String jndiName = (String)values.get(JNDI_NAME_ATTR);
427             String defaultWorkspaceName = (String)values.get(DEFAULT_WORKSPACE_NAME_ATTR);
428             Object defaultCachePolicy = values.get(DEFAULT_CACHE_POLICY_ATTR);
429             String retryLimit = (String)values.get(RETRY_LIMIT_ATTR);
430 
431             String combinedWorkspaceNames = (String)values.get(PREDEFINED_WORKSPACE_NAMES);
432             String[] workspaceNames = null;
433             if (combinedWorkspaceNames != null) {
434                 List<String> paths = StringUtil.splitLines(combinedWorkspaceNames);
435                 workspaceNames = paths.toArray(new String[paths.size()]);
436             }
437 
438             // Create the source instance ...
439             InMemoryRepositorySource source = new InMemoryRepositorySource();
440             if (sourceName != null) source.setName(sourceName);
441             if (rootNodeUuidString != null) source.setRootNodeUuid(UUID.fromString(rootNodeUuidString));
442             if (defaultWorkspaceName != null) source.setDefaultWorkspaceName(defaultWorkspaceName);
443             if (jndiName != null) source.setJndiName(jndiName);
444             if (defaultCachePolicy instanceof CachePolicy) {
445                 source.setDefaultCachePolicy((CachePolicy)defaultCachePolicy);
446             }
447             if (workspaceNames != null && workspaceNames.length != 0) source.setPredefinedWorkspaceNames(workspaceNames);
448             if (retryLimit != null) source.setRetryLimit(Integer.parseInt(retryLimit));
449             return source;
450         }
451         return null;
452     }
453 
454     /**
455      * Gets the names of the workspaces that are available when this source is created.
456      * 
457      * @return the names of the workspaces that this source starts with, or null if there are no such workspaces
458      * @see #setPredefinedWorkspaceNames(String[])
459      */
460     public synchronized String[] getPredefinedWorkspaceNames() {
461         String[] copy = new String[predefinedWorkspaces.length];
462         System.arraycopy(predefinedWorkspaces, 0, copy, 0, predefinedWorkspaces.length);
463         return copy;
464     }
465 
466     /**
467      * Sets the names of the workspaces that are available when this source is created.
468      * 
469      * @param predefinedWorkspaceNames the names of the workspaces that this source should start with, or null if there are no
470      *        such workspaces
471      * @see #getPredefinedWorkspaceNames()
472      */
473     public synchronized void setPredefinedWorkspaceNames( String[] predefinedWorkspaceNames ) {
474         this.predefinedWorkspaces = predefinedWorkspaceNames;
475     }
476 
477     /**
478      * {@inheritDoc}
479      * 
480      * @see org.modeshape.graph.connector.RepositorySource#getCapabilities()
481      */
482     public RepositorySourceCapabilities getCapabilities() {
483         return CAPABILITIES;
484     }
485 
486     public boolean areUpdatesAllowed() {
487         return true;
488     }
489 
490     /**
491      * In-memory connectors aren't shared and cannot be loaded from external sources if updates are not allowed. Therefore, in
492      * order to avoid setting up an in-memory connector that is permanently empty (presumably, not a desired outcome), all
493      * in-memory connectors must allow updates.
494      * 
495      * @param updatesAllowed must be true
496      * @throws RepositorySourceException if {@code updatesAllowed != true}.
497      */
498     public void setUpdatesAllowed( boolean updatesAllowed ) {
499         if (updatesAllowed == false) {
500             throw new RepositorySourceException(GraphI18n.inMemoryConnectorMustAllowUpdates.text(this.name));
501         }
502 
503     }
504 
505     /**
506      * {@inheritDoc}
507      * 
508      * @see java.lang.Object#toString()
509      */
510     @Override
511     public String toString() {
512         return "The \"" + name + "\" in-memory repository";
513     }
514 }