View Javadoc

1   /*
2    * Copyright 2008 University Corporation for Advanced Internet Development, Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package edu.internet2.middleware.shibboleth.common.resource;
18  
19  import java.io.File;
20  import java.io.FileInputStream;
21  import java.io.IOException;
22  import java.io.InputStream;
23  
24  import org.joda.time.DateTime;
25  import org.opensaml.util.resource.AbstractFilteredResource;
26  import org.opensaml.util.resource.ResourceException;
27  import org.opensaml.xml.util.DatatypeHelper;
28  import org.slf4j.Logger;
29  import org.slf4j.LoggerFactory;
30  import org.tmatesoft.svn.core.SVNDepth;
31  import org.tmatesoft.svn.core.SVNException;
32  import org.tmatesoft.svn.core.SVNURL;
33  import org.tmatesoft.svn.core.internal.io.dav.DAVRepositoryFactory;
34  import org.tmatesoft.svn.core.internal.io.fs.FSRepositoryFactory;
35  import org.tmatesoft.svn.core.internal.io.svn.SVNRepositoryFactoryImpl;
36  import org.tmatesoft.svn.core.wc.ISVNStatusHandler;
37  import org.tmatesoft.svn.core.wc.SVNClientManager;
38  import org.tmatesoft.svn.core.wc.SVNRevision;
39  import org.tmatesoft.svn.core.wc.SVNStatus;
40  
41  /**
42   * A resource representing a file fetch from a Subversion server.
43   * 
44   * This resource will fetch the given resource as follows:
45   * <ul>
46   * <li>If the revision is a positive number the resource will fetch the resource once during construction time and will
47   * never attempt to fetch it again.</li>
48   * <li>If the revision number is zero or less, signaling the HEAD revision, every call this resource will cause the
49   * resource to check to see if the current working copy is the same as the revision in the remote repository. If it is
50   * not the new revision will be retrieved.</li>
51   * </ul>
52   * 
53   * The behavior of multiple {@link SVNResource} operating on the same local copy are undefined.
54   * 
55   * @since 1.1
56   */
57  public class SVNResource extends AbstractFilteredResource {
58  
59      /** Class logger. */
60      private final Logger log = LoggerFactory.getLogger(SVNResource.class);
61  
62      /** SVN Client manager. */
63      private final SVNClientManager clientManager;
64  
65      /** URL to the remote repository. */
66      private SVNURL remoteRepository;
67  
68      /** Directory where the working copy will be kept. */
69      private File workingCopyDirectory;
70  
71      /** Revision of the working copy. */
72      private SVNRevision retrievalRevision;
73  
74      /** File, within the working copy, represented by this resource. */
75      private String resourceFileName;
76  
77      /** Time the resource file was last modified. */
78      private DateTime lastModified;
79  
80      /**
81       * Constructor.
82       * 
83       * @param svnClientMgr manager used to create SVN clients
84       * @param repositoryUrl URL of the remote repository
85       * @param workingCopy directory that will serve as the root of the local working copy
86       * @param workingRevision revision of the resource to retrieve or -1 for HEAD revision
87       * @param resourceFile file, within the working copy, represented by this resource
88       * 
89       * @throws ResourceException thrown if there is a problem initializing the SVN resource
90       */
91      public SVNResource(SVNClientManager svnClientMgr, SVNURL repositoryUrl, File workingCopy, long workingRevision,
92              String resourceFile) throws ResourceException {
93          DAVRepositoryFactory.setup();
94          SVNRepositoryFactoryImpl.setup();
95          FSRepositoryFactory.setup();
96          if (svnClientMgr == null) {
97              log.error("SVN client manager may not be null");
98              throw new IllegalArgumentException("SVN client manager may not be null");
99          }
100         clientManager = svnClientMgr;
101 
102         if (repositoryUrl == null) {
103             throw new IllegalArgumentException("SVN repository URL may not be null");
104         }
105         remoteRepository = repositoryUrl;
106 
107         try {
108             checkWorkingCopyDirectory(workingCopy);
109             workingCopyDirectory = workingCopy;
110         } catch (ResourceException e) {
111             throw new IllegalArgumentException(e.getMessage());
112         }
113 
114         if (workingRevision < 0) {
115             this.retrievalRevision = SVNRevision.HEAD;
116         } else {
117             this.retrievalRevision = SVNRevision.create(workingRevision);
118         }
119 
120         resourceFileName = DatatypeHelper.safeTrimOrNullString(resourceFile);
121         if (resourceFileName == null) {
122             log.error("SVN working copy resource file name may not be null or empty");
123             throw new IllegalArgumentException("SVN working copy resource file name may not be null or empty");
124         }
125 
126         checkoutOrUpdateResource();
127         if (!getResourceFile().exists()) {
128             log.error("Resource file " + resourceFile + " does not exist in SVN working copy directory "
129                     + workingCopy.getAbsolutePath());
130             throw new ResourceException("Resource file " + resourceFile
131                     + " does not exist in SVN working copy directory " + workingCopy.getAbsolutePath());
132         }
133     }
134 
135     /** {@inheritDoc} */
136     public boolean exists() throws ResourceException {
137         return getResourceFile().exists();
138     }
139 
140     /** {@inheritDoc} */
141     public InputStream getInputStream() throws ResourceException {
142         checkoutOrUpdateResource();
143         try {
144             return applyFilter(new FileInputStream(getResourceFile()));
145         } catch (IOException e) {
146             String erroMsg = "Unable to read resource file " + resourceFileName + " from local working copy "
147                     + workingCopyDirectory.getAbsolutePath();
148             log.error(erroMsg, e);
149             throw new ResourceException(erroMsg, e);
150         }
151     }
152 
153     /** {@inheritDoc} */
154     public DateTime getLastModifiedTime() throws ResourceException {
155         checkoutOrUpdateResource();
156         return lastModified;
157     }
158 
159     /** {@inheritDoc} */
160     public String getLocation() {
161         return remoteRepository.toDecodedString() + "/" + resourceFileName;
162     }
163 
164     /**
165      * Gets {@link File} for the resource.
166      * 
167      * @return file for the resource
168      * 
169      * @throws ResourceException thrown if there is a problem fetching the resource or checking on its status
170      */
171     protected File getResourceFile() throws ResourceException {
172         return new File(workingCopyDirectory, resourceFileName);
173     }
174 
175     /**
176      * Checks that the given file exists, or can be created, is a directory, and is read/writable by this process.
177      * 
178      * @param directory the directory to check
179      * 
180      * @throws ResourceException thrown if the file is invalid
181      */
182     protected void checkWorkingCopyDirectory(File directory) throws ResourceException {
183         if (directory == null) {
184             log.error("SVN working copy directory may not be null");
185             throw new ResourceException("SVN working copy directory may not be null");
186         }
187 
188         if (!directory.exists()) {
189             boolean created = directory.mkdirs();
190             if (!created) {
191                 log.error("SVN working copy direction " + directory.getAbsolutePath()
192                         + " does not exist and could not be created");
193                 throw new ResourceException("SVN working copy direction " + directory.getAbsolutePath()
194                         + " does not exist and could not be created");
195             }
196         }
197 
198         if (!directory.isDirectory()) {
199             log.error("SVN working copy location " + directory.getAbsolutePath() + " is not a directory");
200             throw new ResourceException("SVN working copy location " + directory.getAbsolutePath()
201                     + " is not a directory");
202         }
203 
204         if (!directory.canRead()) {
205             log.error("SVN working copy directory " + directory.getAbsolutePath() + " can not be read by this process");
206             throw new ResourceException("SVN working copy directory " + directory.getAbsolutePath()
207                     + " can not be read by this process");
208         }
209 
210         if (!directory.canWrite()) {
211             log.error("SVN working copy directory " + directory.getAbsolutePath()
212                     + " can not be written to by this process");
213             throw new ResourceException("SVN working copy directory " + directory.getAbsolutePath()
214                     + " can not be written to by this process");
215         }
216     }
217 
218     /**
219      * Checks out the resource specified by the {@link #remoteRepository} in to the working copy
220      * {@link #workingCopyDirectory}. If the working copy is empty than an SVN checkout is performed if the working copy
221      * already exists then an SVN update is performed.
222      * 
223      * @throws ResourceException thrown if there is a problem communicating with the remote repository, the revision
224      *             does not exist, or the working copy is unusable
225      */
226     protected void checkoutOrUpdateResource() throws ResourceException {
227         log.debug("checking out or updating working copy");
228         SVNRevision newRevision;
229 
230         if (!workingCopyDirectoryExists()) {
231             log.debug("working copy does not yet exist, checking it out");
232             newRevision = checkoutResourceDirectory();
233         } else {
234             if (retrievalRevision != SVNRevision.HEAD) {
235                 log.debug("Working copy exists and version is pegged at {}, no need to update",
236                         retrievalRevision.toString());
237                 return;
238             }
239             log.debug("Working copy exists, updating to latest version.");
240             newRevision = updateResourceDirectory();
241         }
242 
243         log.debug("Determing last modification date of revision {}", newRevision.getNumber());
244         lastModified = getLastModificationForRevision(newRevision);
245     }
246 
247     /**
248      * Checks to see if the working copy directory exists.
249      * 
250      * @return true if the working copy directory exists, false otherwise
251      */
252     private boolean workingCopyDirectoryExists() {
253         File svnMetadataDir = new File(workingCopyDirectory, ".svn");
254         return svnMetadataDir.exists();
255     }
256 
257     /**
258      * Fetches the content from the SVN repository and creates the local working copy.
259      * 
260      * @return the revision of the fetched content
261      * 
262      * @throws ResourceException thrown if there is a problem checking out the content from the repository
263      */
264     private SVNRevision checkoutResourceDirectory() throws ResourceException {
265         try {
266             long newRevision = clientManager.getUpdateClient().doCheckout(remoteRepository, workingCopyDirectory,
267                     retrievalRevision, retrievalRevision, SVNDepth.INFINITY, true);
268             log.debug(
269                     "Checked out revision {} from remote repository {} and stored it in local working directory {}",
270                     new Object[] { newRevision, remoteRepository.toDecodedString(),
271                             workingCopyDirectory.getAbsolutePath(), });
272             return SVNRevision.create(newRevision);
273         } catch (SVNException e) {
274             String errMsg = "Unable to check out revsion " + retrievalRevision.toString() + " from remote repository "
275                     + remoteRepository.toDecodedString() + " to local working directory "
276                     + workingCopyDirectory.getAbsolutePath();
277             log.error(errMsg, e);
278             throw new ResourceException(errMsg, e);
279         }
280     }
281 
282     /**
283      * Updates an existing local working copy from the repository.
284      * 
285      * @return the revision of the fetched content
286      * 
287      * @throws ResourceException thrown if there is a problem updating the working copy
288      */
289     private SVNRevision updateResourceDirectory() throws ResourceException {
290         try {
291             long newRevision = clientManager.getUpdateClient().doUpdate(workingCopyDirectory, retrievalRevision,
292                     SVNDepth.INFINITY, true, true);
293             log.debug("Updated local working directory {} to revision {} from remote repository {}", new Object[] {
294                     workingCopyDirectory.getAbsolutePath(), newRevision, remoteRepository.toDecodedString(), });
295             return SVNRevision.create(newRevision);
296         } catch (SVNException e) {
297             String errMsg = "Unable to update working copy of resoure " + remoteRepository.toDecodedString()
298                     + " in working copy " + workingCopyDirectory.getAbsolutePath() + " to revsion "
299                     + retrievalRevision.toString();
300             log.error(errMsg, e);
301             throw new ResourceException(errMsg, e);
302         }
303     }
304 
305     /**
306      * Gets the last modified time for the given revision.
307      * 
308      * @param revision revision to get the last modified date for
309      * 
310      * @return the last modified time
311      * 
312      * @throws ResourceException thrown if there is a problem getting the last modified time
313      */
314     private DateTime getLastModificationForRevision(SVNRevision revision) throws ResourceException {
315         try {
316             SVNStatusHandler handler = new SVNStatusHandler();
317             clientManager.getStatusClient().doStatus(getResourceFile(), revision, SVNDepth.INFINITY, true, true, false,
318                     false, handler, null);
319             SVNStatus status = handler.getStatus();
320 
321             // remote revision is null when using a pegged version or when using HEAD and the version has not changed
322             if (status.getRemoteRevision() == null) {
323                 return new DateTime(status.getCommittedDate());
324             } else {
325                 return new DateTime(status.getRemoteDate());
326             }
327         } catch (SVNException e) {
328             String errMsg = "Unable to check status of resource " + resourceFileName + " within working directory "
329                     + workingCopyDirectory.getAbsolutePath();
330             log.error(errMsg, e);
331             throw new ResourceException(errMsg, e);
332         }
333     }
334 
335     /** Simple {@link ISVNStatusHandler} implementation that just stores and returns the status. */
336     private class SVNStatusHandler implements ISVNStatusHandler {
337 
338         /** Current status of the resource. */
339         private SVNStatus status;
340 
341         /**
342          * Gets the current status of the resource.
343          * 
344          * @return current status of the resource
345          */
346         public SVNStatus getStatus() {
347             return status;
348         }
349 
350         /** {@inheritDoc} */
351         public void handleStatus(SVNStatus currentStatus) throws SVNException {
352             status = currentStatus;
353         }
354     }
355 }