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.SVNException;
31  import org.tmatesoft.svn.core.SVNURL;
32  import org.tmatesoft.svn.core.auth.BasicAuthenticationManager;
33  import org.tmatesoft.svn.core.wc.SVNClientManager;
34  import org.tmatesoft.svn.core.wc.SVNRevision;
35  import org.tmatesoft.svn.core.wc.SVNStatus;
36  import org.tmatesoft.svn.core.wc.SVNStatusClient;
37  import org.tmatesoft.svn.core.wc.SVNStatusType;
38  import org.tmatesoft.svn.core.wc.SVNUpdateClient;
39  
40  /**
41   * A resource representing a file fetch from a Subversion server.
42   * 
43   * This resource will fetch the given resource as follows:
44   * <ul>
45   * <li>If the revision is a positive number the resource will fetch the resource once during construction time and will
46   * never attempt to fetch it again.</li>
47   * <li>If the revision number is zero or less, signaling the HEAD revision, every call this resource will cause the
48   * resource to check to see if the current working copy is the same as the revision in the remote repository. If it is
49   * not the new revision will be retrieved.</li>
50   * </ul>
51   * 
52   * All operations operate on the local working copy.
53   * 
54   * @since 1.1
55   */
56  public class SVNResource extends AbstractFilteredResource {
57  
58      /** Class logger. */
59      private final Logger log = LoggerFactory.getLogger(SVNResource.class);
60  
61      /** SVN Client manager. */
62      private final SVNClientManager svnClientMgr;
63  
64      /** URL of the remote repository with no trailing slash. */
65      private String remoteResource;
66  
67      /** Filesystem path for local working copy with no trailing slash. */
68      private File localResource;
69  
70      /** Revision of the resource. */
71      private SVNRevision revision;
72  
73      /**
74       * Constructor.
75       * 
76       * @param remote URL of the remote resource
77       * @param local file path to where the local working copy will be kept
78       * @param revision revision of the resource to retrieve or -1 for HEAD revision
79       */
80      public SVNResource(String remote, String local, long revision) {
81          svnClientMgr = SVNClientManager.newInstance();
82  
83          remoteResource = DatatypeHelper.safeTrimOrNullString(remote);
84          if (this.remoteResource == null) {
85              throw new IllegalArgumentException("Remote repository location may not be null or empty");
86          }
87  
88          if (DatatypeHelper.isEmpty(local)) {
89              throw new IllegalArgumentException("Local repository location may not be null or empty");
90          }
91          localResource = new File(local);
92  
93          if (revision < 0) {
94              this.revision = SVNRevision.HEAD;
95          } else {
96              this.revision = SVNRevision.create(revision);
97          }
98      }
99  
100     /**
101      * Constructor.
102      * 
103      * @param remote URL of the remote resource
104      * @param local file path to where the local working copy will be kept
105      * @param revision revision of the resource to retrieve or -1 for HEAD revision
106      * @param username username used to authenticate to the remote server
107      * @param password password used to authenticate to the remote server
108      */
109     public SVNResource(String remote, String local, long revision, String username, String password) {
110         svnClientMgr = SVNClientManager.newInstance();
111 
112         if (username != null) {
113             if (password != null) {
114                 BasicAuthenticationManager authnMgr = new BasicAuthenticationManager(username, password);
115                 svnClientMgr.setAuthenticationManager(authnMgr);
116             } else {
117                 throw new IllegalArgumentException(
118                         "Unable to initialize subversion resource.  User name was given but no password was provided.");
119             }
120         }
121 
122         remoteResource = DatatypeHelper.safeTrimOrNullString(remote);
123         if (this.remoteResource == null) {
124             throw new IllegalArgumentException("Remote resource location may not be null or empty");
125         }
126 
127         if (DatatypeHelper.isEmpty(local)) {
128             throw new IllegalArgumentException("Local repository location may not be null or empty");
129         }
130         localResource = new File(local);
131 
132         if (revision < 0) {
133             this.revision = SVNRevision.HEAD;
134         } else {
135             this.revision = SVNRevision.create(revision);
136         }
137     }
138 
139     /** {@inheritDoc} */
140     public boolean exists() throws ResourceException {
141         checkoutOrUpdateResource();
142         return workingCopyExists();
143     }
144 
145     /** {@inheritDoc} */
146     public InputStream getInputStream() throws ResourceException {
147         checkoutOrUpdateResource();
148 
149         try {
150             return new FileInputStream(localResource);
151         } catch (IOException e) {
152             log.error("Unable read local working copy {}", localResource.getAbsolutePath());
153             throw new ResourceException("Unable to read local working copy of configuration file "
154                     + localResource.getAbsolutePath());
155         }
156     }
157 
158     /** {@inheritDoc} */
159     public DateTime getLastModifiedTime() throws ResourceException {
160         SVNStatus status = getResourceStatus();
161         if (status.getContentsStatus() == SVNStatusType.STATUS_NORMAL) {
162             return new DateTime(status.getWorkingContentsDate());
163         } else {
164             throw new ResourceException("SVN resource is in a state which prevents determining last modified date: "
165                     + status.getContentsStatus().toString());
166         }
167     }
168 
169     /** {@inheritDoc} */
170     public String getLocation() {
171         try {
172             return SVNURL.parseURIDecoded(remoteResource).toDecodedString();
173         } catch (SVNException e) {
174             log.error("Unable to represent remote repository URL as a string");
175             return null;
176         }
177     }
178 
179     /**
180      * Checks to see if a working copy of the file already exists.
181      * 
182      * @return true if a working copy of the file exists, false if not
183      */
184     protected boolean workingCopyExists() {
185         return localResource.exists();
186     }
187 
188     /**
189      * Checks out of updates the local resource. Checkout is used of the local resource does not currently exist. An
190      * update is performed if the local working copy revision does not match the configured revision. If the configured
191      * version is the symbolic version HEAD then an update occurs if the current revision is not the most recent
192      * revision within the remote repository.
193      * 
194      * @throws ResourceException thrown if there is a problem communicating with the remote or local repository
195      */
196     protected void checkoutOrUpdateResource() throws ResourceException {
197         SVNStatus status = getResourceStatus();
198 
199         if (!workingCopyExists()) {
200             checkoutResource();
201             return;
202         }
203 
204         if (revision == SVNRevision.HEAD) {
205             if (!revision.equals(status.getRemoteRevision())) {
206                 updateResource();
207             }
208         } else {
209             if (!revision.equals(status.getRevision())) {
210                 updateResource();
211             }
212         }
213     }
214 
215     /**
216      * Gets the current status of the resource.
217      * 
218      * @return current status of the resource
219      * 
220      * @throws ResourceException thrown if there is a problem communicating with the remote or local repository
221      */
222     protected SVNStatus getResourceStatus() throws ResourceException {
223         SVNStatusClient client = svnClientMgr.getStatusClient();
224 
225         try {
226             return client.doStatus(localResource, true);
227         } catch (SVNException e) {
228             log.error("Unable to determine current status of SVN resource {}", new Object[] { localResource
229                     .getAbsolutePath(), }, e);
230             throw new ResourceException("Unable to determine current status of SVN resource");
231         }
232     }
233 
234     /**
235      * Checks out the remote resource and stores it locally.
236      * 
237      * @throws ResourceException thrown if there is a problem communicating with the remote or local repository
238      */
239     protected void checkoutResource() throws ResourceException {
240         SVNUpdateClient client = svnClientMgr.getUpdateClient();
241         client.setIgnoreExternals(false);
242 
243         try {
244             SVNURL remoteResourceLocation = SVNURL.parseURIDecoded(remoteResource);
245             client.doCheckout(remoteResourceLocation, localResource, revision, revision, false);
246         } catch (SVNException e) {
247             log.error("Unable to check out resource {}", new Object[] { remoteResource }, e);
248             throw new ResourceException("Unable to check out resource from repository");
249         }
250     }
251 
252     /**
253      * Updates the current local working copy, replacing it with the revision from the remote repository.
254      * 
255      * @throws ResourceException thrown if there is a problem communicating with the remote or local repository
256      */
257     protected void updateResource() throws ResourceException {
258         SVNUpdateClient client = svnClientMgr.getUpdateClient();
259         client.setIgnoreExternals(false);
260 
261         try {
262             client.doUpdate(localResource, revision, false);
263         } catch (SVNException e) {
264             log.error("Unable to update working copy of resource {} to revision {}", new Object[] { remoteResource,
265                     revision.getNumber(), }, e);
266             throw new ResourceException("Unable to update working copy of resource");
267         }
268     }
269 }