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    }