View Javadoc

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