Stuart McCulloch | bb01437 | 2012-06-07 21:57:32 +0000 | [diff] [blame] | 1 | package aQute.lib.deployer; |
| 2 | |
| 3 | import java.io.*; |
Stuart McCulloch | 55d4dfe | 2012-08-07 10:57:21 +0000 | [diff] [blame^] | 4 | import java.security.*; |
Stuart McCulloch | bb01437 | 2012-06-07 21:57:32 +0000 | [diff] [blame] | 5 | import java.util.*; |
| 6 | import java.util.jar.*; |
| 7 | import java.util.regex.*; |
| 8 | |
Stuart McCulloch | 39cc9ac | 2012-07-16 13:43:38 +0000 | [diff] [blame] | 9 | import aQute.bnd.header.*; |
| 10 | import aQute.bnd.osgi.*; |
Stuart McCulloch | bb01437 | 2012-06-07 21:57:32 +0000 | [diff] [blame] | 11 | import aQute.bnd.service.*; |
Stuart McCulloch | 6a04666 | 2012-07-19 13:11:20 +0000 | [diff] [blame] | 12 | import aQute.bnd.version.*; |
Stuart McCulloch | bb01437 | 2012-06-07 21:57:32 +0000 | [diff] [blame] | 13 | import aQute.lib.io.*; |
Stuart McCulloch | 81d48de | 2012-06-29 19:23:09 +0000 | [diff] [blame] | 14 | import aQute.service.reporter.*; |
Stuart McCulloch | bb01437 | 2012-06-07 21:57:32 +0000 | [diff] [blame] | 15 | |
| 16 | public class FileRepo implements Plugin, RepositoryPlugin, Refreshable, RegistryPlugin { |
| 17 | public final static String LOCATION = "location"; |
| 18 | public final static String READONLY = "readonly"; |
| 19 | public final static String NAME = "name"; |
| 20 | |
Stuart McCulloch | 2286f23 | 2012-06-15 13:27:53 +0000 | [diff] [blame] | 21 | File[] EMPTY_FILES = new File[0]; |
| 22 | protected File root; |
| 23 | Registry registry; |
| 24 | boolean canWrite = true; |
| 25 | Pattern REPO_FILE = Pattern.compile("([-a-zA-z0-9_\\.]+)-([0-9\\.]+|latest)\\.(jar|lib)"); |
| 26 | Reporter reporter; |
| 27 | boolean dirty; |
| 28 | String name; |
Stuart McCulloch | bb01437 | 2012-06-07 21:57:32 +0000 | [diff] [blame] | 29 | |
Stuart McCulloch | 2286f23 | 2012-06-15 13:27:53 +0000 | [diff] [blame] | 30 | public FileRepo() {} |
Stuart McCulloch | bb01437 | 2012-06-07 21:57:32 +0000 | [diff] [blame] | 31 | |
| 32 | public FileRepo(String name, File location, boolean canWrite) { |
| 33 | this.name = name; |
| 34 | this.root = location; |
| 35 | this.canWrite = canWrite; |
| 36 | } |
| 37 | |
| 38 | protected void init() throws Exception { |
| 39 | // for extensions |
| 40 | } |
| 41 | |
Stuart McCulloch | 2286f23 | 2012-06-15 13:27:53 +0000 | [diff] [blame] | 42 | public void setProperties(Map<String,String> map) { |
Stuart McCulloch | bb01437 | 2012-06-07 21:57:32 +0000 | [diff] [blame] | 43 | String location = map.get(LOCATION); |
| 44 | if (location == null) |
| 45 | throw new IllegalArgumentException("Location must be set on a FileRepo plugin"); |
| 46 | |
| 47 | root = new File(location); |
Stuart McCulloch | bb01437 | 2012-06-07 21:57:32 +0000 | [diff] [blame] | 48 | |
| 49 | String readonly = map.get(READONLY); |
| 50 | if (readonly != null && Boolean.valueOf(readonly).booleanValue()) |
| 51 | canWrite = false; |
| 52 | |
| 53 | name = map.get(NAME); |
| 54 | } |
| 55 | |
| 56 | /** |
| 57 | * Get a list of URLs to bundles that are constrained by the bsn and |
| 58 | * versionRange. |
| 59 | */ |
Stuart McCulloch | d482610 | 2012-06-26 16:34:24 +0000 | [diff] [blame] | 60 | private File[] get(String bsn, String versionRange) throws Exception { |
Stuart McCulloch | bb01437 | 2012-06-07 21:57:32 +0000 | [diff] [blame] | 61 | init(); |
| 62 | |
| 63 | // If the version is set to project, we assume it is not |
| 64 | // for us. A project repo will then get it. |
| 65 | if (versionRange != null && versionRange.equals("project")) |
| 66 | return null; |
| 67 | |
| 68 | // |
| 69 | // Check if the entry exists |
| 70 | // |
| 71 | File f = new File(root, bsn); |
| 72 | if (!f.isDirectory()) |
| 73 | return null; |
| 74 | |
| 75 | // |
| 76 | // The version range we are looking for can |
| 77 | // be null (for all) or a version range. |
| 78 | // |
| 79 | VersionRange range; |
| 80 | if (versionRange == null || versionRange.equals("latest")) { |
| 81 | range = new VersionRange("0"); |
| 82 | } else |
| 83 | range = new VersionRange(versionRange); |
| 84 | |
| 85 | // |
| 86 | // Iterator over all the versions for this BSN. |
| 87 | // Create a sorted map over the version as key |
| 88 | // and the file as URL as value. Only versions |
| 89 | // that match the desired range are included in |
| 90 | // this list. |
| 91 | // |
| 92 | File instances[] = f.listFiles(); |
Stuart McCulloch | 2286f23 | 2012-06-15 13:27:53 +0000 | [diff] [blame] | 93 | SortedMap<Version,File> versions = new TreeMap<Version,File>(); |
Stuart McCulloch | bb01437 | 2012-06-07 21:57:32 +0000 | [diff] [blame] | 94 | for (int i = 0; i < instances.length; i++) { |
| 95 | Matcher m = REPO_FILE.matcher(instances[i].getName()); |
| 96 | if (m.matches() && m.group(1).equals(bsn)) { |
| 97 | String versionString = m.group(2); |
| 98 | Version version; |
| 99 | if (versionString.equals("latest")) |
| 100 | version = new Version(Integer.MAX_VALUE); |
| 101 | else |
| 102 | version = new Version(versionString); |
| 103 | |
| 104 | if (range.includes(version) || versionString.equals(versionRange)) |
| 105 | versions.put(version, instances[i]); |
| 106 | } |
| 107 | } |
| 108 | |
| 109 | File[] files = versions.values().toArray(EMPTY_FILES); |
| 110 | if ("latest".equals(versionRange) && files.length > 0) { |
Stuart McCulloch | 2286f23 | 2012-06-15 13:27:53 +0000 | [diff] [blame] | 111 | return new File[] { |
| 112 | files[files.length - 1] |
| 113 | }; |
Stuart McCulloch | bb01437 | 2012-06-07 21:57:32 +0000 | [diff] [blame] | 114 | } |
| 115 | return files; |
| 116 | } |
| 117 | |
| 118 | public boolean canWrite() { |
| 119 | return canWrite; |
| 120 | } |
| 121 | |
Stuart McCulloch | 55d4dfe | 2012-08-07 10:57:21 +0000 | [diff] [blame^] | 122 | protected PutResult putArtifact(File tmpFile, PutOptions options) throws Exception { |
| 123 | assert (tmpFile != null); |
| 124 | assert (options != null); |
Stuart McCulloch | bb01437 | 2012-06-07 21:57:32 +0000 | [diff] [blame] | 125 | |
Stuart McCulloch | 55d4dfe | 2012-08-07 10:57:21 +0000 | [diff] [blame^] | 126 | Jar jar = null; |
| 127 | try { |
| 128 | init(); |
| 129 | dirty = true; |
Stuart McCulloch | bb01437 | 2012-06-07 21:57:32 +0000 | [diff] [blame] | 130 | |
Stuart McCulloch | 55d4dfe | 2012-08-07 10:57:21 +0000 | [diff] [blame^] | 131 | jar = new Jar(tmpFile); |
Stuart McCulloch | bb01437 | 2012-06-07 21:57:32 +0000 | [diff] [blame] | 132 | |
Stuart McCulloch | 55d4dfe | 2012-08-07 10:57:21 +0000 | [diff] [blame^] | 133 | Manifest manifest = jar.getManifest(); |
| 134 | if (manifest == null) |
| 135 | throw new IllegalArgumentException("No manifest in JAR: " + jar); |
Stuart McCulloch | bb01437 | 2012-06-07 21:57:32 +0000 | [diff] [blame] | 136 | |
Stuart McCulloch | 55d4dfe | 2012-08-07 10:57:21 +0000 | [diff] [blame^] | 137 | String bsn = manifest.getMainAttributes().getValue(Analyzer.BUNDLE_SYMBOLICNAME); |
| 138 | if (bsn == null) |
| 139 | throw new IllegalArgumentException("No Bundle SymbolicName set"); |
Stuart McCulloch | bb01437 | 2012-06-07 21:57:32 +0000 | [diff] [blame] | 140 | |
Stuart McCulloch | 55d4dfe | 2012-08-07 10:57:21 +0000 | [diff] [blame^] | 141 | Parameters b = Processor.parseHeader(bsn, null); |
| 142 | if (b.size() != 1) |
| 143 | throw new IllegalArgumentException("Multiple bsn's specified " + b); |
Stuart McCulloch | bb01437 | 2012-06-07 21:57:32 +0000 | [diff] [blame] | 144 | |
Stuart McCulloch | 55d4dfe | 2012-08-07 10:57:21 +0000 | [diff] [blame^] | 145 | for (String key : b.keySet()) { |
| 146 | bsn = key; |
| 147 | if (!Verifier.SYMBOLICNAME.matcher(bsn).matches()) |
| 148 | throw new IllegalArgumentException("Bundle SymbolicName has wrong format: " + bsn); |
| 149 | } |
Stuart McCulloch | 2286f23 | 2012-06-15 13:27:53 +0000 | [diff] [blame] | 150 | |
Stuart McCulloch | 55d4dfe | 2012-08-07 10:57:21 +0000 | [diff] [blame^] | 151 | String versionString = manifest.getMainAttributes().getValue(Analyzer.BUNDLE_VERSION); |
| 152 | Version version; |
| 153 | if (versionString == null) |
| 154 | version = new Version(); |
| 155 | else |
| 156 | version = new Version(versionString); |
Stuart McCulloch | bb01437 | 2012-06-07 21:57:32 +0000 | [diff] [blame] | 157 | |
Stuart McCulloch | a21b9e8 | 2012-08-02 13:26:25 +0000 | [diff] [blame] | 158 | if (reporter != null) |
Stuart McCulloch | 55d4dfe | 2012-08-07 10:57:21 +0000 | [diff] [blame^] | 159 | reporter.trace("bsn=%s version=%s", bsn, version); |
| 160 | |
| 161 | File dir = new File(root, bsn); |
| 162 | if (!dir.exists() && !dir.mkdirs()) { |
| 163 | throw new IOException("Could not create directory " + dir); |
| 164 | } |
| 165 | String fName = bsn + "-" + version.getWithoutQualifier() + ".jar"; |
| 166 | File file = new File(dir, fName); |
| 167 | |
| 168 | boolean renamed = false; |
| 169 | PutResult result = new PutResult(); |
| 170 | |
| 171 | if (reporter != null) |
| 172 | reporter.trace("updating %s ", file.getAbsolutePath()); |
| 173 | if (!file.exists() || file.lastModified() < jar.lastModified()) { |
| 174 | if (file.exists()) { |
| 175 | IO.delete(file); |
| 176 | } |
| 177 | IO.rename(tmpFile, file); |
| 178 | renamed = true; |
| 179 | result.artifact = file.toURI(); |
| 180 | |
| 181 | if (reporter != null) |
| 182 | reporter.progress(-1, "updated " + file.getAbsolutePath()); |
| 183 | |
| 184 | fireBundleAdded(jar, file); |
| 185 | } else { |
| 186 | if (reporter != null) { |
| 187 | reporter.progress(-1, "Did not update " + jar + " because repo has a newer version"); |
| 188 | reporter.trace("NOT Updating " + fName + " (repo is newer)"); |
| 189 | } |
| 190 | } |
| 191 | |
| 192 | File latest = new File(dir, bsn + "-latest.jar"); |
| 193 | boolean latestExists = latest.exists() && latest.isFile(); |
| 194 | boolean latestIsOlder = latestExists && (latest.lastModified() < jar.lastModified()); |
| 195 | if ((options.createLatest && !latestExists) || latestIsOlder) { |
| 196 | if (latestExists) { |
| 197 | IO.delete(latest); |
| 198 | } |
| 199 | if (!renamed) { |
| 200 | IO.rename(tmpFile, latest); |
| 201 | } else { |
| 202 | IO.copy(file, latest); |
| 203 | } |
| 204 | result.latest = latest.toURI(); |
| 205 | } |
| 206 | |
| 207 | return result; |
| 208 | } |
| 209 | finally { |
| 210 | if (jar != null) { |
| 211 | jar.close(); |
Stuart McCulloch | a21b9e8 | 2012-08-02 13:26:25 +0000 | [diff] [blame] | 212 | } |
Stuart McCulloch | bb01437 | 2012-06-07 21:57:32 +0000 | [diff] [blame] | 213 | } |
Stuart McCulloch | 55d4dfe | 2012-08-07 10:57:21 +0000 | [diff] [blame^] | 214 | } |
Stuart McCulloch | bb01437 | 2012-06-07 21:57:32 +0000 | [diff] [blame] | 215 | |
Stuart McCulloch | 55d4dfe | 2012-08-07 10:57:21 +0000 | [diff] [blame^] | 216 | /* a straight copy of this method lives in LocalIndexedRepo */ |
| 217 | public PutResult put(InputStream stream, PutOptions options) throws Exception { |
| 218 | /* both parameters are required */ |
| 219 | if ((stream == null) || (options == null)) { |
| 220 | throw new IllegalArgumentException("No stream and/or options specified"); |
Stuart McCulloch | bb01437 | 2012-06-07 21:57:32 +0000 | [diff] [blame] | 221 | } |
| 222 | |
Stuart McCulloch | 55d4dfe | 2012-08-07 10:57:21 +0000 | [diff] [blame^] | 223 | /* determine if the put is allowed */ |
| 224 | if (!canWrite) { |
| 225 | throw new IOException("Repository is read-only"); |
| 226 | } |
| 227 | |
| 228 | /* the root directory of the repository has to be a directory */ |
| 229 | if (!root.isDirectory()) { |
| 230 | throw new IOException("Repository directory " + root + " is not a directory"); |
| 231 | } |
| 232 | |
| 233 | /* determine if the artifact needs to be verified */ |
| 234 | boolean verifyFetch = (options.digest != null); |
| 235 | boolean verifyPut = !options.allowArtifactChange; |
| 236 | |
| 237 | /* determine which digests are needed */ |
| 238 | boolean needFetchDigest = verifyFetch || verifyPut; |
| 239 | boolean needPutDigest = verifyPut || options.generateDigest; |
| 240 | |
| 241 | /* |
| 242 | * setup a new stream that encapsulates the stream and calculates (when |
| 243 | * needed) the digest |
| 244 | */ |
| 245 | DigestInputStream dis = new DigestInputStream(stream, MessageDigest.getInstance("SHA-1")); |
| 246 | dis.on(needFetchDigest); |
| 247 | |
| 248 | File tmpFile = null; |
| 249 | try { |
| 250 | /* |
| 251 | * copy the artifact from the (new/digest) stream into a temporary |
| 252 | * file in the root directory of the repository |
| 253 | */ |
| 254 | tmpFile = IO.createTempFile(root, "put", ".bnd"); |
| 255 | IO.copy(dis, tmpFile); |
| 256 | |
| 257 | /* get the digest if available */ |
| 258 | byte[] disDigest = needFetchDigest ? dis.getMessageDigest().digest() : null; |
| 259 | |
| 260 | /* verify the digest when requested */ |
| 261 | if (verifyFetch && !MessageDigest.isEqual(options.digest, disDigest)) { |
| 262 | throw new IOException("Retrieved artifact digest doesn't match specified digest"); |
| 263 | } |
| 264 | |
| 265 | /* put the artifact into the repository (from the temporary file) */ |
| 266 | PutResult r = putArtifact(tmpFile, options); |
| 267 | |
| 268 | /* calculate the digest when requested */ |
| 269 | if (needPutDigest && (r.artifact != null)) { |
| 270 | MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); |
| 271 | IO.copy(new File(r.artifact), sha1); |
| 272 | r.digest = sha1.digest(); |
| 273 | } |
| 274 | |
| 275 | /* verify the artifact when requested */ |
| 276 | if (verifyPut && (r.digest != null) && !MessageDigest.isEqual(disDigest, r.digest)) { |
| 277 | File f = new File(r.artifact); |
| 278 | if (f.exists()) { |
| 279 | IO.delete(f); |
| 280 | } |
| 281 | throw new IOException("Stored artifact digest doesn't match specified digest"); |
| 282 | } |
| 283 | |
| 284 | return r; |
| 285 | } |
| 286 | finally { |
| 287 | if (tmpFile != null && tmpFile.exists()) { |
| 288 | IO.delete(tmpFile); |
| 289 | } |
| 290 | } |
Stuart McCulloch | bb01437 | 2012-06-07 21:57:32 +0000 | [diff] [blame] | 291 | } |
| 292 | |
| 293 | protected void fireBundleAdded(Jar jar, File file) { |
| 294 | if (registry == null) |
| 295 | return; |
Stuart McCulloch | 2286f23 | 2012-06-15 13:27:53 +0000 | [diff] [blame] | 296 | List<RepositoryListenerPlugin> listeners = registry.getPlugins(RepositoryListenerPlugin.class); |
Stuart McCulloch | bb01437 | 2012-06-07 21:57:32 +0000 | [diff] [blame] | 297 | for (RepositoryListenerPlugin listener : listeners) { |
| 298 | try { |
| 299 | listener.bundleAdded(this, jar, file); |
Stuart McCulloch | 2286f23 | 2012-06-15 13:27:53 +0000 | [diff] [blame] | 300 | } |
| 301 | catch (Exception e) { |
Stuart McCulloch | bb01437 | 2012-06-07 21:57:32 +0000 | [diff] [blame] | 302 | if (reporter != null) |
| 303 | reporter.warning("Repository listener threw an unexpected exception: %s", e); |
| 304 | } |
| 305 | } |
| 306 | } |
| 307 | |
| 308 | public void setLocation(String string) { |
| 309 | root = new File(string); |
| 310 | if (!root.isDirectory()) |
| 311 | throw new IllegalArgumentException("Invalid repository directory"); |
| 312 | } |
| 313 | |
| 314 | public void setReporter(Reporter reporter) { |
| 315 | this.reporter = reporter; |
| 316 | } |
| 317 | |
| 318 | public List<String> list(String regex) throws Exception { |
| 319 | init(); |
| 320 | Instruction pattern = null; |
| 321 | if (regex != null) |
| 322 | pattern = new Instruction(regex); |
| 323 | |
| 324 | List<String> result = new ArrayList<String>(); |
| 325 | if (root == null) { |
| 326 | if (reporter != null) |
| 327 | reporter.error("FileRepo root directory is not set."); |
| 328 | } else { |
| 329 | File[] list = root.listFiles(); |
| 330 | if (list != null) { |
| 331 | for (File f : list) { |
| 332 | if (!f.isDirectory()) |
| 333 | continue; // ignore non-directories |
| 334 | String fileName = f.getName(); |
| 335 | if (fileName.charAt(0) == '.') |
| 336 | continue; // ignore hidden files |
| 337 | if (pattern == null || pattern.matches(fileName)) |
| 338 | result.add(fileName); |
| 339 | } |
| 340 | } else if (reporter != null) |
| 341 | reporter.error("FileRepo root directory (%s) does not exist", root); |
| 342 | } |
| 343 | |
| 344 | return result; |
| 345 | } |
| 346 | |
| 347 | public List<Version> versions(String bsn) throws Exception { |
| 348 | init(); |
| 349 | File dir = new File(root, bsn); |
| 350 | if (dir.isDirectory()) { |
| 351 | String versions[] = dir.list(); |
| 352 | List<Version> list = new ArrayList<Version>(); |
| 353 | for (String v : versions) { |
| 354 | Matcher m = REPO_FILE.matcher(v); |
| 355 | if (m.matches()) { |
| 356 | String version = m.group(2); |
| 357 | if (version.equals("latest")) |
| 358 | version = Integer.MAX_VALUE + ""; |
| 359 | list.add(new Version(version)); |
| 360 | } |
| 361 | } |
| 362 | return list; |
| 363 | } |
| 364 | return null; |
| 365 | } |
| 366 | |
Stuart McCulloch | 55d4dfe | 2012-08-07 10:57:21 +0000 | [diff] [blame^] | 367 | @Override |
Stuart McCulloch | bb01437 | 2012-06-07 21:57:32 +0000 | [diff] [blame] | 368 | public String toString() { |
| 369 | return String.format("%-40s r/w=%s", root.getAbsolutePath(), canWrite()); |
| 370 | } |
| 371 | |
| 372 | public File getRoot() { |
| 373 | return root; |
| 374 | } |
| 375 | |
| 376 | public boolean refresh() { |
| 377 | if (dirty) { |
| 378 | dirty = false; |
| 379 | return true; |
Stuart McCulloch | d482610 | 2012-06-26 16:34:24 +0000 | [diff] [blame] | 380 | } |
| 381 | return false; |
Stuart McCulloch | bb01437 | 2012-06-07 21:57:32 +0000 | [diff] [blame] | 382 | } |
| 383 | |
| 384 | public String getName() { |
| 385 | if (name == null) { |
| 386 | return toString(); |
| 387 | } |
| 388 | return name; |
| 389 | } |
| 390 | |
| 391 | public Jar get(String bsn, Version v) throws Exception { |
| 392 | init(); |
| 393 | File bsns = new File(root, bsn); |
Stuart McCulloch | 2286f23 | 2012-06-15 13:27:53 +0000 | [diff] [blame] | 394 | File version = new File(bsns, bsn + "-" + v.getMajor() + "." + v.getMinor() + "." + v.getMicro() + ".jar"); |
| 395 | if (version.exists()) |
Stuart McCulloch | bb01437 | 2012-06-07 21:57:32 +0000 | [diff] [blame] | 396 | return new Jar(version); |
Stuart McCulloch | d482610 | 2012-06-26 16:34:24 +0000 | [diff] [blame] | 397 | return null; |
Stuart McCulloch | bb01437 | 2012-06-07 21:57:32 +0000 | [diff] [blame] | 398 | } |
| 399 | |
Stuart McCulloch | 2286f23 | 2012-06-15 13:27:53 +0000 | [diff] [blame] | 400 | public File get(String bsn, String version, Strategy strategy, Map<String,String> properties) throws Exception { |
Stuart McCulloch | bb01437 | 2012-06-07 21:57:32 +0000 | [diff] [blame] | 401 | if (version == null) |
| 402 | version = "0.0.0"; |
| 403 | |
| 404 | if (strategy == Strategy.EXACT) { |
| 405 | VersionRange vr = new VersionRange(version); |
| 406 | if (vr.isRange()) |
| 407 | return null; |
| 408 | |
| 409 | if (vr.getHigh().getMajor() == Integer.MAX_VALUE) |
| 410 | version = "latest"; |
| 411 | |
| 412 | File file = IO.getFile(root, bsn + "/" + bsn + "-" + version + ".jar"); |
| 413 | if (file.isFile()) |
| 414 | return file; |
Stuart McCulloch | d482610 | 2012-06-26 16:34:24 +0000 | [diff] [blame] | 415 | file = IO.getFile(root, bsn + "/" + bsn + "-" + version + ".lib"); |
| 416 | if (file.isFile()) |
| 417 | return file; |
Stuart McCulloch | bb01437 | 2012-06-07 21:57:32 +0000 | [diff] [blame] | 418 | return null; |
| 419 | |
| 420 | } |
| 421 | File[] files = get(bsn, version); |
| 422 | if (files == null || files.length == 0) |
| 423 | return null; |
| 424 | |
| 425 | if (files.length >= 0) { |
| 426 | switch (strategy) { |
Stuart McCulloch | 2286f23 | 2012-06-15 13:27:53 +0000 | [diff] [blame] | 427 | case LOWEST : |
| 428 | return files[0]; |
| 429 | case HIGHEST : |
| 430 | return files[files.length - 1]; |
Stuart McCulloch | 151384c | 2012-06-18 11:15:15 +0000 | [diff] [blame] | 431 | case EXACT : |
| 432 | // TODO |
| 433 | break; |
Stuart McCulloch | bb01437 | 2012-06-07 21:57:32 +0000 | [diff] [blame] | 434 | } |
| 435 | } |
| 436 | return null; |
| 437 | } |
| 438 | |
| 439 | public void setRegistry(Registry registry) { |
| 440 | this.registry = registry; |
| 441 | } |
| 442 | |
| 443 | public String getLocation() { |
| 444 | return root.toString(); |
| 445 | } |
| 446 | |
| 447 | } |