001 /*
002 * JBoss DNA (http://www.jboss.org/dna)
003 * See the COPYRIGHT.txt file distributed with this work for information
004 * regarding copyright ownership. Some portions may be licensed
005 * to Red Hat, Inc. under one or more contributor license agreements.
006 * See the AUTHORS.txt file in the distribution for a full listing of
007 * individual contributors.
008 *
009 * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
010 * is licensed to you under the terms of the GNU Lesser General Public License as
011 * published by the Free Software Foundation; either version 2.1 of
012 * the License, or (at your option) any later version.
013 *
014 * JBoss DNA is distributed in the hope that it will be useful,
015 * but WITHOUT ANY WARRANTY; without even the implied warranty of
016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
017 * Lesser General Public License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this software; if not, write to the Free
021 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
022 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
023 */
024 package org.jboss.dna.connector.svn;
025
026 import java.util.Enumeration;
027 import java.util.HashMap;
028 import java.util.Hashtable;
029 import java.util.List;
030 import java.util.Map;
031 import java.util.concurrent.CopyOnWriteArraySet;
032 import javax.naming.Context;
033 import javax.naming.Name;
034 import javax.naming.RefAddr;
035 import javax.naming.Reference;
036 import javax.naming.StringRefAddr;
037 import javax.naming.spi.ObjectFactory;
038 import net.jcip.annotations.Immutable;
039 import net.jcip.annotations.ThreadSafe;
040 import org.jboss.dna.common.i18n.I18n;
041 import org.jboss.dna.common.util.CheckArg;
042 import org.jboss.dna.common.util.Logger;
043 import org.jboss.dna.common.util.StringUtil;
044 import org.jboss.dna.graph.cache.CachePolicy;
045 import org.jboss.dna.graph.connector.RepositoryConnection;
046 import org.jboss.dna.graph.connector.RepositoryContext;
047 import org.jboss.dna.graph.connector.RepositorySource;
048 import org.jboss.dna.graph.connector.RepositorySourceCapabilities;
049 import org.jboss.dna.graph.connector.RepositorySourceException;
050 import org.tmatesoft.svn.core.io.SVNRepository;
051
052 /**
053 * The {@link RepositorySource} for the connector that exposes an area of the local/remote svn repository as content in a
054 * repository. This source considers a workspace name to be the path to the directory on the repository's root directory location
055 * that represents the root of that workspace. New workspaces can be created, as long as the names represent valid paths to
056 * existing directories.
057 *
058 * @author Serge Pagop
059 */
060 @ThreadSafe
061 public class SVNRepositorySource implements RepositorySource, ObjectFactory {
062
063 /**
064 * The first serialized version of this source. Version {@value} .
065 */
066 private static final long serialVersionUID = 1L;
067
068 protected static final String SOURCE_NAME = "sourceName";
069 protected static final String SVN_REPOSITORY_ROOT_URL = "repositoryRootURL";
070 protected static final String SVN_USERNAME = "username";
071 protected static final String SVN_PASSWORD = "password";
072 protected static final String CACHE_TIME_TO_LIVE_IN_MILLISECONDS = "cacheTimeToLiveInMilliseconds";
073 protected static final String RETRY_LIMIT = "retryLimit";
074 protected static final String DEFAULT_WORKSPACE = "defaultWorkspace";
075 protected static final String PREDEFINED_WORKSPACE_NAMES = "predefinedWorkspaceNames";
076 protected static final String ALLOW_CREATING_WORKSPACES = "allowCreatingWorkspaces";
077
078 /**
079 * This source supports events.
080 */
081 protected static final boolean SUPPORTS_EVENTS = true;
082 /**
083 * This source supports same-name-siblings.
084 */
085 protected static final boolean SUPPORTS_SAME_NAME_SIBLINGS = false;
086 /**
087 * This source does support creating workspaces.
088 */
089 protected static final boolean DEFAULT_SUPPORTS_CREATING_WORKSPACES = true;
090 /**
091 * This source supports udpates by default, but each instance may be configured to be read-only or updateable}.
092 */
093 public static final boolean DEFAULT_SUPPORTS_UPDATES = false;
094
095 /**
096 * This source supports creating references.
097 */
098 protected static final boolean SUPPORTS_REFERENCES = false;
099
100 public static final int DEFAULT_RETRY_LIMIT = 0;
101 public static final int DEFAULT_CACHE_TIME_TO_LIVE_IN_SECONDS = 60 * 5; // 5
102 // minutes
103
104 private volatile String name;
105 private volatile String repositoryRootURL;
106 private volatile String username;
107 private volatile String password;
108 private volatile int retryLimit = DEFAULT_RETRY_LIMIT;
109 private volatile int cacheTimeToLiveInMilliseconds = DEFAULT_CACHE_TIME_TO_LIVE_IN_SECONDS * 1000;
110 private volatile String defaultWorkspace;
111 private volatile String[] predefinedWorkspaces = new String[] {};
112 private volatile RepositorySourceCapabilities capabilities = new RepositorySourceCapabilities(
113 SUPPORTS_SAME_NAME_SIBLINGS,
114 DEFAULT_SUPPORTS_UPDATES,
115 SUPPORTS_EVENTS,
116 DEFAULT_SUPPORTS_CREATING_WORKSPACES,
117 SUPPORTS_REFERENCES);
118
119 private transient CachePolicy cachePolicy;
120 private transient CopyOnWriteArraySet<String> availableWorspaceNames;
121
122 /**
123 * Create a repository source instance.
124 */
125 public SVNRepositorySource() {
126 }
127
128 /**
129 * {@inheritDoc}
130 *
131 * @see org.jboss.dna.graph.connector.RepositorySource#getCapabilities()
132 */
133 public RepositorySourceCapabilities getCapabilities() {
134 return capabilities;
135 }
136
137 /**
138 * {@inheritDoc}
139 */
140 public String getName() {
141 return this.name;
142 }
143
144 /**
145 * Set the name for the source
146 *
147 * @param name the new name for the source
148 */
149 public synchronized void setName( String name ) {
150 if (name != null) {
151 name = name.trim();
152 if (name.length() == 0) name = null;
153 }
154 this.name = name;
155 }
156
157 /**
158 * @return the url
159 */
160 public String getRepositoryRootURL() {
161 return this.repositoryRootURL;
162 }
163
164 /**
165 * Set the url for the subversion repository.
166 *
167 * @param url - the url location.
168 * @throws IllegalArgumentException If svn url is null or empty
169 */
170 public void setRepositoryRootURL( String url ) {
171 CheckArg.isNotEmpty(url, "RepositoryRootURL");
172 this.repositoryRootURL = url;
173 }
174
175 public String getUsername() {
176 return this.username;
177 }
178
179 /**
180 * @param username
181 */
182 public void setUsername( String username ) {
183 this.username = username;
184 }
185
186 public String getPassword() {
187 return this.password;
188 }
189
190 /**
191 * @param password
192 */
193 public void setPassword( String password ) {
194 this.password = password;
195 }
196
197 /**
198 * Get whether this source supports updates.
199 *
200 * @return true if this source supports updates, or false if this source only supports reading content.
201 */
202 public boolean getSupportsUpdates() {
203 return capabilities.supportsUpdates();
204 }
205
206 /**
207 * Get the file system path to the existing directory that should be used for the default workspace. If the default is
208 * specified as a null String or is not a valid and resolvable path, this source will consider the default to be the current
209 * working directory of this virtual machine, as defined by the <code>new File(".")</code>.
210 *
211 * @return the file system path to the directory representing the default workspace, or null if the default should be the
212 * current working directory
213 */
214 public String getDirectoryForDefaultWorkspace() {
215 return defaultWorkspace;
216 }
217
218 /**
219 * Set the file system path to the existing directory that should be used for the default workspace. If the default is
220 * specified as a null String or is not a valid and resolvable path, this source will consider the default to be the current
221 * working directory of this virtual machine, as defined by the <code>new File(".")</code>.
222 *
223 * @param pathToDirectoryForDefaultWorkspace the valid and resolvable file system path to the directory representing the
224 * default workspace, or null if the current working directory should be used as the default workspace
225 */
226 public synchronized void setDirectoryForDefaultWorkspace( String pathToDirectoryForDefaultWorkspace ) {
227 this.defaultWorkspace = pathToDirectoryForDefaultWorkspace;
228 }
229
230 /**
231 * Gets the names of the workspaces that are available when this source is created. Each workspace name corresponds to a path
232 * to a directory on the file system.
233 *
234 * @return the names of the workspaces that this source starts with, or null if there are no such workspaces
235 * @see #setPredefinedWorkspaceNames(String[])
236 * @see #setCreatingWorkspacesAllowed(boolean)
237 */
238 public synchronized String[] getPredefinedWorkspaceNames() {
239 String[] copy = new String[predefinedWorkspaces.length];
240 System.arraycopy(predefinedWorkspaces, 0, copy, 0, predefinedWorkspaces.length);
241 return copy;
242 }
243
244 /**
245 * Sets the names of the workspaces that are available when this source is created. Each workspace name corresponds to a path
246 * to a directory on the file system.
247 *
248 * @param predefinedWorkspaceNames the names of the workspaces that this source should start with, or null if there are no
249 * such workspaces
250 * @see #setCreatingWorkspacesAllowed(boolean)
251 * @see #getPredefinedWorkspaceNames()
252 */
253 public synchronized void setPredefinedWorkspaceNames( String[] predefinedWorkspaceNames ) {
254 this.predefinedWorkspaces = predefinedWorkspaceNames;
255 }
256
257 /**
258 * Get whether this source allows workspaces to be created dynamically.
259 *
260 * @return true if this source allows workspaces to be created by clients, or false if the set of workspaces is fixed
261 * @see #setPredefinedWorkspaceNames(String[])
262 * @see #getPredefinedWorkspaceNames()
263 * @see #setCreatingWorkspacesAllowed(boolean)
264 */
265 public boolean isCreatingWorkspacesAllowed() {
266 return capabilities.supportsCreatingWorkspaces();
267 }
268
269 /**
270 * Set whether this source allows workspaces to be created dynamically.
271 *
272 * @param allowWorkspaceCreation true if this source allows workspaces to be created by clients, or false if the set of
273 * workspaces is fixed
274 * @see #setPredefinedWorkspaceNames(String[])
275 * @see #getPredefinedWorkspaceNames()
276 * @see #isCreatingWorkspacesAllowed()
277 */
278 public synchronized void setCreatingWorkspacesAllowed( boolean allowWorkspaceCreation ) {
279 capabilities = new RepositorySourceCapabilities(capabilities.supportsSameNameSiblings(), capabilities.supportsUpdates(),
280 capabilities.supportsEvents(), allowWorkspaceCreation,
281 capabilities.supportsReferences());
282 }
283
284 /**
285 * {@inheritDoc}
286 *
287 * @see org.jboss.dna.graph.connector.RepositorySource#getRetryLimit()
288 */
289 public int getRetryLimit() {
290 return retryLimit;
291 }
292
293 /**
294 * {@inheritDoc}
295 *
296 * @see org.jboss.dna.graph.connector.RepositorySource#setRetryLimit(int)
297 */
298 public void setRetryLimit( int limit ) {
299 retryLimit = limit < 0 ? 0 : limit;
300 }
301
302 /**
303 * Get the time in milliseconds that content returned from this source may used while in the cache.
304 *
305 * @return the time to live, in milliseconds, or 0 if the time to live is not specified by this source
306 */
307 public int getCacheTimeToLiveInMilliseconds() {
308 return cacheTimeToLiveInMilliseconds;
309 }
310
311 /**
312 * Set the time in milliseconds that content returned from this source may used while in the cache.
313 *
314 * @param cacheTimeToLive the time to live, in milliseconds; 0 if the time to live is not specified by this source; or a
315 * negative number for the default value
316 */
317 public synchronized void setCacheTimeToLiveInMilliseconds( int cacheTimeToLive ) {
318 if (cacheTimeToLive < 0) cacheTimeToLive = DEFAULT_CACHE_TIME_TO_LIVE_IN_SECONDS;
319 this.cacheTimeToLiveInMilliseconds = cacheTimeToLive;
320 this.cachePolicy = cacheTimeToLiveInMilliseconds > 0 ? new SVNRepositoryCachePolicy(cacheTimeToLiveInMilliseconds) : null;
321
322 }
323
324 /**
325 * {@inheritDoc}
326 *
327 * @see org.jboss.dna.graph.connector.RepositorySource#initialize(org.jboss.dna.graph.connector.RepositoryContext)
328 */
329 public synchronized void initialize( RepositoryContext context ) throws RepositorySourceException {
330 // No need to do anything
331 }
332
333 /**
334 * {@inheritDoc}
335 */
336 @Override
337 public boolean equals( Object obj ) {
338 if (obj == this) return true;
339 if (obj instanceof SVNRepositorySource) {
340 SVNRepositorySource that = (SVNRepositorySource)obj;
341 if (this.getName() == null) {
342 if (that.getName() != null) return false;
343 } else {
344 if (!this.getName().equals(that.getName())) return false;
345 }
346 return true;
347 }
348 return false;
349 }
350
351 /**
352 * {@inheritDoc}
353 *
354 * @see javax.naming.Referenceable#getReference()
355 */
356 public synchronized Reference getReference() {
357 String className = getClass().getName();
358 String factoryClassName = this.getClass().getName();
359 Reference ref = new Reference(className, factoryClassName, null);
360
361 if (getName() != null) {
362 ref.add(new StringRefAddr(SOURCE_NAME, getName()));
363 }
364 if (getRepositoryRootURL() != null) {
365 ref.add(new StringRefAddr(SVN_REPOSITORY_ROOT_URL, getRepositoryRootURL()));
366 }
367 if (getUsername() != null) {
368 ref.add(new StringRefAddr(SVN_USERNAME, getUsername()));
369 }
370 if (getPassword() != null) {
371 ref.add(new StringRefAddr(SVN_PASSWORD, getPassword()));
372 }
373 ref.add(new StringRefAddr(CACHE_TIME_TO_LIVE_IN_MILLISECONDS, Integer.toString(getCacheTimeToLiveInMilliseconds())));
374 ref.add(new StringRefAddr(RETRY_LIMIT, Integer.toString(getRetryLimit())));
375 ref.add(new StringRefAddr(DEFAULT_WORKSPACE, getDirectoryForDefaultWorkspace()));
376 ref.add(new StringRefAddr(ALLOW_CREATING_WORKSPACES, Boolean.toString(isCreatingWorkspacesAllowed())));
377 String[] workspaceNames = getPredefinedWorkspaceNames();
378 if (workspaceNames != null && workspaceNames.length != 0) {
379 ref.add(new StringRefAddr(PREDEFINED_WORKSPACE_NAMES, StringUtil.combineLines(workspaceNames)));
380 }
381 return ref;
382
383 }
384
385 /**
386 * {@inheritDoc}
387 *
388 * @see javax.naming.spi.ObjectFactory#getObjectInstance(java.lang.Object, javax.naming.Name, javax.naming.Context,
389 * java.util.Hashtable)
390 */
391 public Object getObjectInstance( Object obj,
392 Name name,
393 Context nameCtx,
394 Hashtable<?, ?> environment ) throws Exception {
395 if (obj instanceof Reference) {
396 Map<String, String> values = new HashMap<String, String>();
397 Reference ref = (Reference)obj;
398 Enumeration<?> en = ref.getAll();
399 while (en.hasMoreElements()) {
400 RefAddr subref = (RefAddr)en.nextElement();
401 if (subref instanceof StringRefAddr) {
402 String key = subref.getType();
403 Object value = subref.getContent();
404 if (value != null) values.put(key, value.toString());
405 }
406 }
407 String sourceName = values.get(SOURCE_NAME);
408 String repositoryRootURL = values.get(SVN_REPOSITORY_ROOT_URL);
409 String username = values.get(SVN_USERNAME);
410 String password = values.get(SVN_PASSWORD);
411 String cacheTtlInMillis = values.get(CACHE_TIME_TO_LIVE_IN_MILLISECONDS);
412 String retryLimit = values.get(RETRY_LIMIT);
413 String defaultWorkspace = values.get(DEFAULT_WORKSPACE);
414 String createWorkspaces = values.get(ALLOW_CREATING_WORKSPACES);
415
416 String combinedWorkspaceNames = values.get(PREDEFINED_WORKSPACE_NAMES);
417 String[] workspaceNames = null;
418 if (combinedWorkspaceNames != null) {
419 List<String> paths = StringUtil.splitLines(combinedWorkspaceNames);
420 workspaceNames = paths.toArray(new String[paths.size()]);
421 }
422 // Create the source instance ...
423 SVNRepositorySource source = new SVNRepositorySource();
424 if (sourceName != null) source.setName(sourceName);
425 if (cacheTtlInMillis != null) source.setCacheTimeToLiveInMilliseconds(Integer.parseInt(cacheTtlInMillis));
426 if (repositoryRootURL != null) source.setRepositoryRootURL(repositoryRootURL);
427 if (username != null) source.setUsername(username);
428 if (password != null) source.setPassword(password);
429 if (retryLimit != null) source.setRetryLimit(Integer.parseInt(retryLimit));
430 if (defaultWorkspace != null) source.setDirectoryForDefaultWorkspace(defaultWorkspace);
431 if (createWorkspaces != null) source.setCreatingWorkspacesAllowed(Boolean.parseBoolean(createWorkspaces));
432 if (workspaceNames != null && workspaceNames.length != 0) source.setPredefinedWorkspaceNames(workspaceNames);
433 return source;
434 }
435 return null;
436 }
437
438 /**
439 * {@inheritDoc}
440 *
441 * @see org.jboss.dna.graph.connector.RepositorySource#getConnection()
442 */
443 public RepositoryConnection getConnection() throws RepositorySourceException {
444
445 String sourceName = getName();
446 if (sourceName == null || sourceName.trim().length() == 0) {
447 I18n msg = SVNRepositoryConnectorI18n.propertyIsRequired;
448 throw new RepositorySourceException(getName(), msg.text("name"));
449 }
450
451 String sourceUsername = getUsername();
452 if (sourceUsername == null || sourceUsername.trim().length() == 0) {
453 I18n msg = SVNRepositoryConnectorI18n.propertyIsRequired;
454 throw new RepositorySourceException(getUsername(), msg.text("username"));
455 }
456
457 String sourcePassword = getPassword();
458 if (sourcePassword == null) {
459 I18n msg = SVNRepositoryConnectorI18n.propertyIsRequired;
460 throw new RepositorySourceException(getPassword(), msg.text("password"));
461 }
462
463 String repositoryRootURL = getRepositoryRootURL();
464 if (repositoryRootURL == null || repositoryRootURL.trim().length() == 0) {
465 I18n msg = SVNRepositoryConnectorI18n.propertyIsRequired;
466 throw new RepositorySourceException(getRepositoryRootURL(), msg.text("repositoryRootURL"));
467 }
468
469
470 SVNRepository repos = null;
471 // Report the warnings for non-existant predefined workspaces
472 boolean reportWarnings = false;
473 if (this.availableWorspaceNames == null) {
474 // Set up the predefined workspace names ...
475 this.availableWorspaceNames = new CopyOnWriteArraySet<String>();
476 for (String predefined : this.predefinedWorkspaces) {
477 // if exist e.i trunk/ /branches /tags
478 this.availableWorspaceNames.add(predefined);
479 }
480 // Report the warnings for non-existant predefined workspaces and we
481 // take it that if no predefined workspace exist
482 // we will take the repository root url as a pseudo workspace
483 reportWarnings = true;
484 for (String url : this.availableWorspaceNames) {
485 // check if the predefined workspaces exist.
486 if (repos != null) {
487 SVNRepositoryUtil.setNewSVNRepositoryLocation(repos, url, true, sourceName);
488 } else {
489 repos = SVNRepositoryUtil.createRepository(url, sourceUsername, sourcePassword);
490 }
491 if (!SVNRepositoryUtil.exist(repos)) {
492
493 Logger.getLogger(getClass()).warn(SVNRepositoryConnectorI18n.pathForPredefinedWorkspaceDoesNotExist,
494 url,
495 name);
496 }
497 if (!SVNRepositoryUtil.isDirectory(repos,"")) {
498 Logger.getLogger(getClass()).warn(SVNRepositoryConnectorI18n.pathForPredefinedWorkspaceIsNotDirectory,
499 url,
500 name);
501 }
502 }
503 }
504
505 boolean supportsUpdates = getSupportsUpdates();
506
507 SVNRepository defaultWorkspace = null;
508 if (repos != null) {
509 SVNRepositoryUtil.setNewSVNRepositoryLocation(repos, getRepositoryRootURL(), true, sourceName);
510 defaultWorkspace = repos;
511 } else {
512 defaultWorkspace = SVNRepositoryUtil.createRepository(getRepositoryRootURL(), sourceUsername, sourcePassword);
513 }
514
515 String defaultURL = getDirectoryForDefaultWorkspace();
516 if (defaultURL != null) {
517 // Look for the entry at this path .....
518 SVNRepository repository = SVNRepositoryUtil.createRepository(defaultURL,
519 sourceUsername,
520 sourcePassword);
521 I18n warning = null;
522 if (!SVNRepositoryUtil.exist(repository)) {
523 warning = SVNRepositoryConnectorI18n.pathForPredefinedWorkspaceDoesNotExist;
524 } else if (!SVNRepositoryUtil.isDirectory(repository,"")) {
525 warning = SVNRepositoryConnectorI18n.pathForPredefinedWorkspaceIsNotDirectory;
526 } else {
527 // is a directory and is good to use!
528 defaultWorkspace = repository;
529 }
530 if (reportWarnings && warning != null) {
531 Logger.getLogger(getClass()).warn(warning, defaultURL, name);
532 }
533 }
534 this.availableWorspaceNames.add(defaultWorkspace.getLocation().toDecodedString());
535 return new SVNRepositoryConnection(name, defaultWorkspace, availableWorspaceNames, isCreatingWorkspacesAllowed(),
536 cachePolicy, supportsUpdates, new RepositoryAccessData(getRepositoryRootURL(),
537 sourceUsername, sourcePassword));
538 }
539
540 @Immutable
541 /* package */class SVNRepositoryCachePolicy implements CachePolicy {
542 private static final long serialVersionUID = 1L;
543 private final int ttl;
544
545 /* package */SVNRepositoryCachePolicy( int ttl ) {
546 this.ttl = ttl;
547 }
548
549 public long getTimeToLive() {
550 return ttl;
551 }
552
553 }
554 }