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  import org.tmatesoft.svn.core.wc.SVNStatusClient;
41  import org.tmatesoft.svn.core.wc.SVNUpdateClient;
42  
43  /**
44   * A resource representing a file fetch from a Subversion server.
45   * 
46   * This resource will fetch the given resource as follows:
47   * <ul>
48   * <li>If the revision is a positive number the resource will fetch the resource once during construction time and will
49   * never attempt to fetch it again.</li>
50   * <li>If the revision number is zero or less, signaling the HEAD revision, every call this resource will cause the
51   * resource to check to see if the current working copy is the same as the revision in the remote repository. If it is
52   * not the new revision will be retrieved.</li>
53   * </ul>
54   * 
55   * All operations operate on the local working copy.
56   * 
57   * @since 1.1
58   */
59  public class SVNResource extends AbstractFilteredResource {
60  
61      /** Class logger. */
62      private final Logger log = LoggerFactory.getLogger(SVNResource.class);
63  
64      /** SVN Client manager. */
65      private final SVNClientManager clientManager;
66  
67      /** URL to the remote repository. */
68      private SVNURL remoteRepository;
69  
70      /** Directory where the working copy will be kept. */
71      private File workingCopy;
72  
73      /** Revision of the working copy. */
74      private SVNRevision revision;
75  
76      /** File, within the working copy, represented by this resource. */
77      private String resourceFileName;
78  
79      /**
80       * Constructor.
81       * 
82       * @param svnClientMgr manager used to create SVN clients
83       * @param repositoryUrl URL of the remote repository
84       * @param workingCopyDirectory directory that will serve as the root of the local working copy
85       * @param workingRevision revision of the resource to retrieve or -1 for HEAD revision
86       * @param resourceFile file, within the working copy, represented by this resource
87       * 
88       * @throws ResourceException thrown if there is a problem initializing the SVN resource
89       */
90      public SVNResource(SVNClientManager svnClientMgr, SVNURL repositoryUrl, File workingCopyDirectory,
91              long workingRevision, String resourceFile) throws ResourceException {
92          DAVRepositoryFactory.setup();
93          SVNRepositoryFactoryImpl.setup();
94          FSRepositoryFactory.setup();
95          if (svnClientMgr == null) {
96              log.error("SVN client manager may not be null");
97              throw new IllegalArgumentException("SVN client manager may not be null");
98          }
99          clientManager = svnClientMgr;
100 
101         if (repositoryUrl == null) {
102             throw new IllegalArgumentException("SVN repository URL may not be null");
103         }
104         remoteRepository = repositoryUrl;
105 
106         try {
107             checkWorkingCopyDirectory(workingCopyDirectory);
108             workingCopy = workingCopyDirectory;
109         } catch (ResourceException e) {
110             throw new IllegalArgumentException(e.getMessage());
111         }
112 
113         if (workingRevision < 0) {
114             this.revision = SVNRevision.HEAD;
115         } else {
116             this.revision = SVNRevision.create(workingRevision);
117         }
118 
119         resourceFileName = DatatypeHelper.safeTrimOrNullString(resourceFile);
120         if (resourceFileName == null) {
121             log.error("SVN working copy resource file name may not be null or empty");
122             throw new IllegalArgumentException("SVN working copy resource file name may not be null or empty");
123         }
124 
125         checkoutOrUpdateResource();
126         if (!getResourceFile().exists()) {
127             log.error("Resource file " + resourceFile + " does not exist in SVN working copy directory "
128                     + workingCopyDirectory.getAbsolutePath());
129             throw new ResourceException("Resource file " + resourceFile
130                     + " does not exist in SVN working copy directory " + workingCopyDirectory.getAbsolutePath());
131         }
132     }
133 
134     /** {@inheritDoc} */
135     public boolean exists() throws ResourceException {
136         return getResourceFile().exists();
137     }
138 
139     /** {@inheritDoc} */
140     public InputStream getInputStream() throws ResourceException {
141         try {
142             return applyFilter(new FileInputStream(getResourceFile()));
143         } catch (IOException e) {
144             String erroMsg = "Unable to read resource file " + resourceFileName + " from local working copy "
145                     + workingCopy.getAbsolutePath();
146             log.error(erroMsg, e);
147             throw new ResourceException(erroMsg, e);
148         }
149     }
150 
151     /** {@inheritDoc} */
152     public DateTime getLastModifiedTime() throws ResourceException {
153         SVNStatusClient client = clientManager.getStatusClient();
154         client.setIgnoreExternals(false);
155 
156         try {
157             SVNStatusHandler handler = new SVNStatusHandler();
158             client.doStatus(getResourceFile(), revision, SVNDepth.INFINITY, true, true, false, false, handler, null);
159             SVNStatus status = handler.getStatus();
160 
161             // remote revision is null when using a pegged version or when using HEAD and the version has not changed
162             if (status.getRemoteRevision() == null) {
163                 return new DateTime(status.getCommittedDate());
164             } else {
165                 return new DateTime(status.getRemoteDate());
166             }
167         } catch (SVNException e) {
168             String errMsg = "Unable to check status of resource " + resourceFileName + " within working directory "
169                     + workingCopy.getAbsolutePath();
170             log.error(errMsg, e);
171             throw new ResourceException(errMsg, e);
172         }
173     }
174 
175     /** {@inheritDoc} */
176     public String getLocation() {
177         return remoteRepository.toDecodedString() + "/" + resourceFileName;
178     }
179 
180     /**
181      * Checks that the given file exists, or can be created, is a directory, and is read/writable by this process.
182      * 
183      * @param directory the directory to check
184      * 
185      * @throws ResourceException thrown if the file is invalid
186      */
187     protected void checkWorkingCopyDirectory(File directory) throws ResourceException {
188         if (directory == null) {
189             log.error("SVN working copy directory may not be null");
190             throw new ResourceException("SVN working copy directory may not be null");
191         }
192 
193         if (!directory.exists()) {
194             boolean created = directory.mkdirs();
195             if (!created) {
196                 log.error("SVN working copy direction " + directory.getAbsolutePath()
197                         + " does not exist and could not be created");
198                 throw new ResourceException("SVN working copy direction " + directory.getAbsolutePath()
199                         + " does not exist and could not be created");
200             }
201         }
202 
203         if (!directory.isDirectory()) {
204             log.error("SVN working copy location " + directory.getAbsolutePath() + " is not a directory");
205             throw new ResourceException("SVN working copy location " + directory.getAbsolutePath()
206                     + " is not a directory");
207         }
208 
209         if (!directory.canRead()) {
210             log.error("SVN working copy directory " + directory.getAbsolutePath() + " can not be read by this process");
211             throw new ResourceException("SVN working copy directory " + directory.getAbsolutePath()
212                     + " can not be read by this process");
213         }
214 
215         if (!directory.canWrite()) {
216             log.error("SVN working copy directory " + directory.getAbsolutePath()
217                     + " can not be written to by this process");
218             throw new ResourceException("SVN working copy directory " + directory.getAbsolutePath()
219                     + " can not be written to by this process");
220         }
221     }
222 
223     /**
224      * Checks out the resource specified by the {@link #remoteRepository} in to the working copy {@link #workingCopy}.
225      * If the working copy is empty than an SVN checkout is performed if the working copy already exists then an SVN
226      * update is performed.
227      * 
228      * @throws ResourceException thrown if there is a problem communicating with the remote repository, the revision
229      *             does not exist, or the working copy is unusable
230      */
231     protected void checkoutOrUpdateResource() throws ResourceException {
232         SVNUpdateClient client = clientManager.getUpdateClient();
233         client.setIgnoreExternals(false);
234 
235         File svnMetadataDir = new File(workingCopy, ".svn");
236         if (!svnMetadataDir.exists()) {
237             try {
238                 client.doCheckout(remoteRepository, workingCopy, revision, revision, SVNDepth.INFINITY, true);
239             } catch (SVNException e) {
240                 String errMsg = "Unable to check out revsion " + revision.toString() + " from remote repository "
241                         + remoteRepository.toDecodedString() + " to local working directory "
242                         + workingCopy.getAbsolutePath();
243                 log.error(errMsg, e);
244                 throw new ResourceException(errMsg, e);
245             }
246         } else {
247             try {
248                 client.doUpdate(workingCopy, revision, SVNDepth.INFINITY, true, true);
249             } catch (SVNException e) {
250                 String errMsg = "Unable to update working copy of resoure " + remoteRepository.toDecodedString()
251                         + " in working copy " + workingCopy.getAbsolutePath() + " to revsion " + revision.toString();
252                 log.error(errMsg, e);
253                 throw new ResourceException(errMsg, e);
254             }
255         }
256     }
257 
258     /**
259      * Gets {@link File} for the resource.
260      * 
261      * @return file for the resource
262      */
263     protected File getResourceFile() {
264         return new File(workingCopy, resourceFileName);
265     }
266 
267     /** Simple {@link ISVNStatusHandler} implementation that just stores and returns the status. */
268     private class SVNStatusHandler implements ISVNStatusHandler {
269 
270         /** Current status of the resource. */
271         private SVNStatus status;
272 
273         /**
274          * Gets the current status of the resource.
275          * 
276          * @return current status of the resource
277          */
278         public SVNStatus getStatus() {
279             return status;
280         }
281 
282         /** {@inheritDoc} */
283         public void handleStatus(SVNStatus currentStatus) throws SVNException {
284             status = currentStatus;
285         }
286     }
287 }