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 }