├── .gitignore ├── LICENSE.txt ├── README.md ├── pom.xml └── src ├── main └── java │ ├── io │ └── takari │ │ ├── aether │ │ ├── concurrency │ │ │ ├── LockingFileProcessor.java │ │ │ ├── LockingSyncContext.java │ │ │ └── LockingSyncContextFactory.java │ │ └── localrepo │ │ │ ├── ArtifactUnavailableException.java │ │ │ ├── ArtifactValidationException.java │ │ │ ├── ArtifactValidator.java │ │ │ ├── BaseLocalRepositoryManager.java │ │ │ ├── TakariLocalRepositoryManager.java │ │ │ ├── TakariLocalRepositoryManagerFactory.java │ │ │ ├── TakariUpdateCheckManager.java │ │ │ └── TrackingFileManager.java │ │ └── filemanager │ │ ├── FileManager.java │ │ ├── Lock.java │ │ └── internal │ │ └── DefaultFileManager.java │ └── org │ └── eclipse │ └── aether │ └── internal │ └── impl │ └── DefaultUpdateCheckManager.java └── test ├── java └── io │ └── takari │ ├── aether │ └── localrepo │ │ ├── BaseLocalRepositoryManagerTest.java │ │ ├── SimpleResolutionErrorPolicy.java │ │ ├── TakariLocalRepositoryManagerTest.java │ │ ├── TakariUpdateCheckManagerTest.java │ │ ├── TestFileUtils.java │ │ ├── TrackingFileManagerTest.java │ │ └── its │ │ └── TakariLocalRepositoryTest.java │ └── filemanager │ ├── DefaultFileManagerTest.java │ ├── ExternalProcessFileLock.java │ ├── ExternalProcessFileLocks.java │ ├── ForkJvm.java │ ├── MultipleThreadsLockManagerTest.java │ └── TestFileUtils.java └── projects └── basic-it └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .project 3 | .classpath 4 | .settings/ 5 | bin/ 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 1.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 4 | 5 | 1. DEFINITIONS 6 | 7 | "Contribution" means: 8 | 9 | a) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and 10 | b) in the case of each subsequent Contributor: 11 | i) changes to the Program, and 12 | ii) additions to the Program; 13 | where such changes and/or additions to the Program originate from and are distributed by that particular Contributor. A Contribution 'originates' from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include additions to the Program which: (i) are separate modules of software distributed in conjunction with the Program under their own license agreement, and (ii) are not derivative works of the Program. 14 | "Contributor" means any person or entity that distributes the Program. 15 | 16 | "Licensed Patents" mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. 17 | 18 | "Program" means the Contributions distributed in accordance with this Agreement. 19 | 20 | "Recipient" means anyone who receives the Program under this Agreement, including all Contributors. 21 | 22 | 2. GRANT OF RIGHTS 23 | 24 | a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, distribute and sublicense the Contribution of such Contributor, if any, and such derivative works, in source code and object code form. 25 | b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in source code and object code form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. 26 | c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program. 27 | d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. 28 | 3. REQUIREMENTS 29 | 30 | A Contributor may choose to distribute the Program in object code form under its own license agreement, provided that: 31 | 32 | a) it complies with the terms and conditions of this Agreement; and 33 | b) its license agreement: 34 | i) effectively disclaims on behalf of all Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; 35 | ii) effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; 36 | iii) states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any other party; and 37 | iv) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange. 38 | When the Program is made available in source code form: 39 | 40 | a) it must be made available under this Agreement; and 41 | b) a copy of this Agreement must be included with each copy of the Program. 42 | Contributors may not remove or alter any copyright notices contained within the Program. 43 | 44 | Each Contributor must identify itself as the originator of its Contribution, if any, in a manner that reasonably allows subsequent Recipients to identify the originator of the Contribution. 45 | 46 | 4. COMMERCIAL DISTRIBUTION 47 | 48 | Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense. 49 | 50 | For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages. 51 | 52 | 5. NO WARRANTY 53 | 54 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement , including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations. 55 | 56 | 6. DISCLAIMER OF LIABILITY 57 | 58 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 59 | 60 | 7. GENERAL 61 | 62 | If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. 63 | 64 | If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. 65 | 66 | All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. 67 | 68 | Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved. 69 | 70 | This Agreement is governed by the laws of the State of New York and the intellectual property laws of the United States of America. No party to this Agreement will bring a legal action under this Agreement more than one year after the cause of action arose. Each party waives its rights to a jury trial in any resulting litigation. 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Takari Concurrent Local Repository 2 | 3 | This project is being archived, while main features are today provided by Maven Resolver 1.7+, see here https://maven.apache.org/resolver/local-repository.html#shared-access-to-local-repository 4 | 5 | The Takari Concurrent Local Repository component is a replacement for the management of a local Maven repository 6 | of a default Maven installation. It makes access to the local repository safe for concurrent access from multiple 7 | threads and Maven invocations. 8 | 9 | Documentation for usage and more is available in the Takari TEAM documentation at http://takari.io/book/index.html 10 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 4.0.0 10 | 11 | io.takari 12 | takari 13 | 28 14 | 15 | takari-maven-component 16 | io.takari.aether 17 | takari-local-repository 18 | 0.11.4-SNAPSHOT 19 | 20 | Takari :: Local Repository 21 | 22 | This extension for Aether contains a synchronization context that employs OS-level file locks to enable safe 23 | concurrent access to the local repository across processes. 24 | 25 | 2010 26 | 27 | 28 | 1.0.0.v20140518 29 | 30 | 31 | 32 | 33 | javax.inject 34 | javax.inject 35 | 1 36 | provided 37 | 38 | 39 | org.eclipse.aether 40 | aether-api 41 | ${aetherVersion} 42 | provided 43 | 44 | 45 | org.eclipse.aether 46 | aether-spi 47 | ${aetherVersion} 48 | provided 49 | 50 | 51 | org.eclipse.aether 52 | aether-impl 53 | ${aetherVersion} 54 | provided 55 | 56 | 57 | org.slf4j 58 | slf4j-api 59 | 1.7.5 60 | provided 61 | 62 | 63 | com.google.guava 64 | guava 65 | 18.0 66 | provided 67 | 68 | 69 | org.eclipse.aether 70 | aether-test-util 71 | ${aetherVersion} 72 | test 73 | 74 | 75 | junit 76 | junit 77 | 4.11 78 | test 79 | 80 | 81 | com.googlecode.multithreadedtc 82 | multithreadedtc 83 | 1.01 84 | test 85 | 86 | 87 | org.slf4j 88 | slf4j-simple 89 | 1.7.5 90 | test 91 | 92 | 93 | org.codehaus.plexus 94 | plexus-utils 95 | 3.0.20 96 | test 97 | 98 | 99 | io.takari.maven.plugins 100 | takari-plugin-testing 101 | 2.7.0 102 | test 103 | 104 | 105 | io.takari.maven.plugins 106 | takari-plugin-integration-testing 107 | 2.7.0 108 | pom 109 | test 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /src/main/java/io/takari/aether/concurrency/LockingFileProcessor.java: -------------------------------------------------------------------------------- 1 | package io.takari.aether.concurrency; 2 | 3 | /******************************************************************************* 4 | * Copyright (c) 2010-2014 Takari, Inc., Sonatype, Inc. 5 | * All rights reserved. This program and the accompanying materials 6 | * are made available under the terms of the Eclipse Public License v1.0 7 | * which accompanies this distribution, and is available at 8 | * http://www.eclipse.org/legal/epl-v10.html 9 | *******************************************************************************/ 10 | 11 | import io.takari.filemanager.FileManager; 12 | 13 | import java.io.File; 14 | import java.io.IOException; 15 | import java.io.InputStream; 16 | import java.nio.ByteBuffer; 17 | 18 | import javax.inject.Inject; 19 | import javax.inject.Named; 20 | import javax.inject.Singleton; 21 | 22 | import org.eclipse.aether.spi.io.FileProcessor; 23 | 24 | /** 25 | * A {@link FileProcessor} implementation that delegates all important operations to {@link DefaultFileLockManager}. 26 | * 27 | * @author Jason van Zyl 28 | */ 29 | @Named 30 | @Singleton 31 | public class LockingFileProcessor implements FileProcessor { 32 | 33 | private FileManager fileManager; 34 | 35 | @Inject 36 | public LockingFileProcessor(FileManager fileManager) { 37 | this.fileManager = fileManager; 38 | } 39 | 40 | @Override 41 | public boolean mkdirs(File directory) { 42 | return fileManager.mkdirs(directory); 43 | } 44 | 45 | @Override 46 | public void write(File target, String data) throws IOException { 47 | fileManager.write(target, data); 48 | 49 | } 50 | 51 | @Override 52 | public void write(File target, InputStream source) throws IOException { 53 | fileManager.write(target, source); 54 | } 55 | 56 | @Override 57 | public void move(File source, File target) throws IOException { 58 | fileManager.move(source, target); 59 | } 60 | 61 | @Override 62 | public void copy(File source, File target) throws IOException { 63 | fileManager.copy(source, target); 64 | } 65 | 66 | @Override 67 | public long copy(File source, File target, ProgressListener listener) throws IOException { 68 | return fileManager.copy(source, target, new ProgressListenerAdapter(listener)); 69 | } 70 | 71 | static class ProgressListenerAdapter implements io.takari.filemanager.FileManager.ProgressListener { 72 | 73 | private ProgressListener listener; 74 | 75 | public ProgressListenerAdapter(ProgressListener listener) { 76 | this.listener = listener; 77 | } 78 | 79 | @Override 80 | public void progressed(ByteBuffer buffer) throws IOException { 81 | listener.progressed(buffer); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/io/takari/aether/concurrency/LockingSyncContext.java: -------------------------------------------------------------------------------- 1 | package io.takari.aether.concurrency; 2 | 3 | /******************************************************************************* 4 | * Copyright (c) 2010-2014 Takari, Inc., Sonatype, Inc. 5 | * All rights reserved. This program and the accompanying materials 6 | * are made available under the terms of the Eclipse Public License v1.0 7 | * which accompanies this distribution, and is available at 8 | * http://www.eclipse.org/legal/epl-v10.html 9 | *******************************************************************************/ 10 | 11 | import io.takari.filemanager.FileManager; 12 | import io.takari.filemanager.Lock; 13 | 14 | import java.io.File; 15 | import java.io.IOException; 16 | import java.util.Collection; 17 | import java.util.LinkedHashMap; 18 | import java.util.Map; 19 | import java.util.TreeSet; 20 | 21 | import org.eclipse.aether.RepositorySystemSession; 22 | import org.eclipse.aether.SyncContext; 23 | import org.eclipse.aether.artifact.Artifact; 24 | import org.eclipse.aether.metadata.Metadata; 25 | import org.eclipse.aether.repository.LocalRepositoryManager; 26 | import org.slf4j.Logger; 27 | import org.slf4j.LoggerFactory; 28 | 29 | class LockingSyncContext implements SyncContext { 30 | private static final char SEPARATOR = '~'; 31 | 32 | private Logger logger = LoggerFactory.getLogger(LockingFileProcessor.class); 33 | 34 | private final FileManager fileLockManager; 35 | private final LocalRepositoryManager localRepoMan; 36 | private final boolean shared; 37 | private final Map locks = new LinkedHashMap(); 38 | 39 | public LockingSyncContext(boolean shared, RepositorySystemSession session, FileManager fileLockManager, Logger logger) { 40 | this.shared = shared; 41 | this.logger = logger; 42 | this.fileLockManager = fileLockManager; 43 | this.localRepoMan = session.getLocalRepositoryManager(); 44 | } 45 | 46 | public void acquire(Collection artifacts, Collection metadatas) { 47 | Collection paths = new TreeSet(); 48 | addArtifactPaths(paths, artifacts); 49 | addMetadataPaths(paths, metadatas); 50 | File basedir = getLockBasedir(); 51 | for (String path : paths) { 52 | File file = new File(basedir, path); 53 | Lock lock = locks.get(path); 54 | if (lock == null) { 55 | if (shared) { 56 | lock = fileLockManager.readLock(file); 57 | } else { 58 | lock = fileLockManager.writeLock(file); 59 | } 60 | locks.put(path, lock); 61 | try { 62 | lock.lock(); 63 | } catch (IOException e) { 64 | logger.warn("Failed to lock file " + lock.getFile() + ": " + e); 65 | } 66 | } 67 | } 68 | } 69 | 70 | private File getLockBasedir() { 71 | return new File(localRepoMan.getRepository().getBasedir(), ".locks"); 72 | } 73 | 74 | private void addArtifactPaths(Collection paths, Collection artifacts) { 75 | if (artifacts != null) { 76 | for (Artifact artifact : artifacts) { 77 | String path = getPath(artifact); 78 | paths.add(path); 79 | } 80 | } 81 | } 82 | 83 | private String getPath(Artifact artifact) { 84 | // NOTE: Don't use LRM.getPath*() as those paths could be different across processes, e.g. due to staging LRMs. 85 | StringBuilder path = new StringBuilder(128); 86 | path.append(artifact.getGroupId()).append(SEPARATOR); 87 | path.append(artifact.getArtifactId()).append(SEPARATOR); 88 | path.append(artifact.getBaseVersion()); 89 | return path.toString(); 90 | } 91 | 92 | private void addMetadataPaths(Collection paths, Collection metadatas) { 93 | if (metadatas != null) { 94 | for (Metadata metadata : metadatas) { 95 | String path = getPath(metadata); 96 | paths.add(path); 97 | } 98 | } 99 | } 100 | 101 | private String getPath(Metadata metadata) { 102 | // NOTE: Don't use LRM.getPath*() as those paths could be different across processes, e.g. due to staging. 103 | StringBuilder path = new StringBuilder(128); 104 | if (metadata.getGroupId().length() > 0) { 105 | path.append(metadata.getGroupId()); 106 | if (metadata.getArtifactId().length() > 0) { 107 | path.append(SEPARATOR).append(metadata.getArtifactId()); 108 | if (metadata.getVersion().length() > 0) { 109 | path.append(SEPARATOR).append(metadata.getVersion()); 110 | } 111 | } 112 | } 113 | return path.toString(); 114 | } 115 | 116 | public void close() { 117 | for (Lock lock : locks.values()) { 118 | try { 119 | lock.unlock(); 120 | } catch (IOException e) { 121 | logger.warn("Failed to unlock file " + lock.getFile() + ": " + e); 122 | } 123 | } 124 | locks.clear(); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/io/takari/aether/concurrency/LockingSyncContextFactory.java: -------------------------------------------------------------------------------- 1 | package io.takari.aether.concurrency; 2 | 3 | /******************************************************************************* 4 | * Copyright (c) 2010-2014 Takari, Inc., Sonatype, Inc. 5 | * All rights reserved. This program and the accompanying materials 6 | * are made available under the terms of the Eclipse Public License v1.0 7 | * which accompanies this distribution, and is available at 8 | * http://www.eclipse.org/legal/epl-v10.html 9 | *******************************************************************************/ 10 | 11 | import io.takari.filemanager.FileManager; 12 | 13 | import javax.inject.Inject; 14 | import javax.inject.Named; 15 | import javax.inject.Singleton; 16 | 17 | import org.eclipse.aether.RepositorySystemSession; 18 | import org.eclipse.aether.SyncContext; 19 | import org.eclipse.aether.impl.SyncContextFactory; 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | 23 | /** 24 | * A synchronization context factory that employs OS-level file locks to control access to artifacts/metadatas. 25 | */ 26 | @Named 27 | @Singleton 28 | public class LockingSyncContextFactory implements SyncContextFactory { 29 | 30 | private Logger logger = LoggerFactory.getLogger(LockingFileProcessor.class); 31 | 32 | private FileManager fileLockManager; 33 | 34 | @Inject 35 | public LockingSyncContextFactory(FileManager fileLockManager) { 36 | this.fileLockManager = fileLockManager; 37 | } 38 | 39 | public SyncContext newInstance(RepositorySystemSession session, boolean shared) { 40 | return new LockingSyncContext(shared, session, fileLockManager, logger); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/io/takari/aether/localrepo/ArtifactUnavailableException.java: -------------------------------------------------------------------------------- 1 | package io.takari.aether.localrepo; 2 | 3 | /** 4 | * Artifact validation exception that blocks artifact resolution but does not trigger immediate 5 | * build failure. 6 | */ 7 | public class ArtifactUnavailableException extends ArtifactValidationException { 8 | private static final long serialVersionUID = 9197584795220384513L; 9 | 10 | public ArtifactUnavailableException(String message) { 11 | super(message); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/io/takari/aether/localrepo/ArtifactValidationException.java: -------------------------------------------------------------------------------- 1 | package io.takari.aether.localrepo; 2 | 3 | public class ArtifactValidationException extends RuntimeException { 4 | 5 | public ArtifactValidationException(String message) { 6 | super(message); 7 | } 8 | 9 | private static final long serialVersionUID = 974526790521636680L; 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/io/takari/aether/localrepo/ArtifactValidator.java: -------------------------------------------------------------------------------- 1 | package io.takari.aether.localrepo; 2 | 3 | import org.eclipse.aether.artifact.Artifact; 4 | import org.eclipse.aether.repository.LocalRepository; 5 | import org.eclipse.aether.repository.RemoteRepository; 6 | 7 | public interface ArtifactValidator { 8 | 9 | void validateOnAdd(Artifact artifact, LocalRepository localRepository, RemoteRepository remoteRepository) throws ArtifactValidationException; 10 | 11 | void validateOnFind(Artifact artifact, LocalRepository localRepository, RemoteRepository remoteRepository) throws ArtifactValidationException; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/io/takari/aether/localrepo/BaseLocalRepositoryManager.java: -------------------------------------------------------------------------------- 1 | package io.takari.aether.localrepo; 2 | 3 | /******************************************************************************* 4 | * Copyright (c) 2010, 2011 Sonatype, Inc. 5 | * All rights reserved. This program and the accompanying materials 6 | * are made available under the terms of the Eclipse Public License v1.0 7 | * which accompanies this distribution, and is available at 8 | * http://www.eclipse.org/legal/epl-v10.html 9 | * 10 | * Contributors: 11 | * Sonatype, Inc. - initial API and implementation 12 | *******************************************************************************/ 13 | 14 | import java.io.File; 15 | import java.io.UnsupportedEncodingException; 16 | import java.security.MessageDigest; 17 | import java.security.NoSuchAlgorithmException; 18 | import java.util.SortedSet; 19 | import java.util.TreeSet; 20 | 21 | import org.eclipse.aether.RepositorySystemSession; 22 | import org.eclipse.aether.artifact.Artifact; 23 | import org.eclipse.aether.metadata.Metadata; 24 | import org.eclipse.aether.repository.LocalArtifactRegistration; 25 | import org.eclipse.aether.repository.LocalArtifactRequest; 26 | import org.eclipse.aether.repository.LocalArtifactResult; 27 | import org.eclipse.aether.repository.LocalMetadataRegistration; 28 | import org.eclipse.aether.repository.LocalMetadataRequest; 29 | import org.eclipse.aether.repository.LocalMetadataResult; 30 | import org.eclipse.aether.repository.LocalRepository; 31 | import org.eclipse.aether.repository.LocalRepositoryManager; 32 | import org.eclipse.aether.repository.RemoteRepository; 33 | 34 | /** 35 | * A local repository manager that realizes the classical Maven 2.0 local repository. 36 | */ 37 | class BaseLocalRepositoryManager implements LocalRepositoryManager { 38 | 39 | private final LocalRepository repository; 40 | 41 | public BaseLocalRepositoryManager(File basedir) { 42 | this(basedir, "simple"); 43 | } 44 | 45 | public BaseLocalRepositoryManager(String basedir) { 46 | this((basedir != null) ? new File(basedir) : null, "simple"); 47 | } 48 | 49 | BaseLocalRepositoryManager(File basedir, String type) { 50 | if (basedir == null) { 51 | throw new IllegalArgumentException("base directory has not been specified"); 52 | } 53 | repository = new LocalRepository(basedir.getAbsoluteFile(), type); 54 | } 55 | 56 | public LocalRepository getRepository() { 57 | return repository; 58 | } 59 | 60 | String getPathForArtifact(Artifact artifact, boolean local) { 61 | StringBuilder path = new StringBuilder(128); 62 | path.append(artifact.getGroupId().replace('.', '/')).append('/'); 63 | path.append(artifact.getArtifactId()).append('/'); 64 | path.append(artifact.getBaseVersion()).append('/'); 65 | path.append(artifact.getArtifactId()).append('-'); 66 | if (local) { 67 | path.append(artifact.getBaseVersion()); 68 | } else { 69 | path.append(artifact.getVersion()); 70 | } 71 | if (artifact.getClassifier().length() > 0) { 72 | path.append('-').append(artifact.getClassifier()); 73 | } 74 | if (artifact.getExtension().length() > 0) { 75 | path.append('.').append(artifact.getExtension()); 76 | } 77 | return path.toString(); 78 | } 79 | 80 | public String getPathForLocalArtifact(Artifact artifact) { 81 | return getPathForArtifact(artifact, true); 82 | } 83 | 84 | public String getPathForRemoteArtifact(Artifact artifact, RemoteRepository repository, String context) { 85 | return getPathForArtifact(artifact, false); 86 | } 87 | 88 | public String getPathForLocalMetadata(Metadata metadata) { 89 | return getPath(metadata, "local"); 90 | } 91 | 92 | public String getPathForRemoteMetadata(Metadata metadata, RemoteRepository repository, String context) { 93 | return getPath(metadata, getRepositoryKey(repository, context)); 94 | } 95 | 96 | String getRepositoryKey(RemoteRepository repository, String context) { 97 | String key; 98 | if (repository.isRepositoryManager()) { 99 | // repository serves dynamic contents, take request parameters into account for key 100 | StringBuilder buffer = new StringBuilder(128); 101 | buffer.append(repository.getId()); 102 | buffer.append('-'); 103 | 104 | SortedSet subKeys = new TreeSet(); 105 | for (RemoteRepository mirroredRepo : repository.getMirroredRepositories()) { 106 | subKeys.add(mirroredRepo.getId()); 107 | } 108 | 109 | SimpleDigest digest = new SimpleDigest(); 110 | digest.update(context); 111 | for (String subKey : subKeys) { 112 | digest.update(subKey); 113 | } 114 | buffer.append(digest.digest()); 115 | 116 | key = buffer.toString(); 117 | } else { 118 | // repository serves static contents, its id is sufficient as key 119 | key = repository.getId(); 120 | } 121 | 122 | return key; 123 | } 124 | 125 | private String getPath(Metadata metadata, String repositoryKey) { 126 | StringBuilder path = new StringBuilder(128); 127 | 128 | if (metadata.getGroupId().length() > 0) { 129 | path.append(metadata.getGroupId().replace('.', '/')).append('/'); 130 | 131 | if (metadata.getArtifactId().length() > 0) { 132 | path.append(metadata.getArtifactId()).append('/'); 133 | 134 | if (metadata.getVersion().length() > 0) { 135 | path.append(metadata.getVersion()).append('/'); 136 | } 137 | } 138 | } 139 | 140 | path.append(insertRepositoryKey(metadata.getType(), repositoryKey)); 141 | 142 | return path.toString(); 143 | } 144 | 145 | private String insertRepositoryKey(String filename, String repositoryKey) { 146 | String result; 147 | int idx = filename.indexOf('.'); 148 | if (idx < 0) { 149 | result = filename + '-' + repositoryKey; 150 | } else { 151 | result = filename.substring(0, idx) + '-' + repositoryKey + filename.substring(idx); 152 | } 153 | return result; 154 | } 155 | 156 | public LocalArtifactResult find(RepositorySystemSession session, LocalArtifactRequest request) { 157 | String path = getPathForArtifact(request.getArtifact(), false); 158 | File file = new File(getRepository().getBasedir(), path); 159 | 160 | LocalArtifactResult result = new LocalArtifactResult(request); 161 | if (file.isFile()) { 162 | result.setFile(file); 163 | result.setAvailable(true); 164 | } 165 | 166 | return result; 167 | } 168 | 169 | public void add(RepositorySystemSession session, LocalArtifactRegistration request) { 170 | // noop 171 | } 172 | 173 | @Override 174 | public String toString() { 175 | return String.valueOf(getRepository()); 176 | } 177 | 178 | public LocalMetadataResult find(RepositorySystemSession session, LocalMetadataRequest request) { 179 | LocalMetadataResult result = new LocalMetadataResult(request); 180 | 181 | String path; 182 | 183 | Metadata metadata = request.getMetadata(); 184 | String context = request.getContext(); 185 | RemoteRepository remote = request.getRepository(); 186 | 187 | if (remote != null) { 188 | path = getPathForRemoteMetadata(metadata, remote, context); 189 | } else { 190 | path = getPathForLocalMetadata(metadata); 191 | } 192 | 193 | File file = new File(getRepository().getBasedir(), path); 194 | if (file.isFile()) { 195 | result.setFile(file); 196 | } 197 | 198 | return result; 199 | } 200 | 201 | public void add(RepositorySystemSession session, LocalMetadataRegistration request) { 202 | // noop 203 | } 204 | 205 | // 206 | // Just use Guava 207 | // 208 | class SimpleDigest { 209 | 210 | private MessageDigest digest; 211 | 212 | private long hash; 213 | 214 | public SimpleDigest() { 215 | try { 216 | digest = MessageDigest.getInstance("SHA-1"); 217 | } catch (NoSuchAlgorithmException e) { 218 | try { 219 | digest = MessageDigest.getInstance("MD5"); 220 | } catch (NoSuchAlgorithmException ne) { 221 | digest = null; 222 | hash = 13; 223 | } 224 | } 225 | } 226 | 227 | public void update(String data) { 228 | if (data == null || data.length() <= 0) { 229 | return; 230 | } 231 | if (digest != null) { 232 | try { 233 | digest.update(data.getBytes("UTF-8")); 234 | } catch (UnsupportedEncodingException e) { 235 | // broken JVM 236 | } 237 | } else { 238 | hash = hash * 31 + data.hashCode(); 239 | } 240 | } 241 | 242 | public String digest() { 243 | if (digest != null) { 244 | StringBuilder buffer = new StringBuilder(64); 245 | 246 | byte[] bytes = digest.digest(); 247 | for (int i = 0; i < bytes.length; i++) { 248 | int b = bytes[i] & 0xFF; 249 | 250 | if (b < 0x10) { 251 | buffer.append('0'); 252 | } 253 | 254 | buffer.append(Integer.toHexString(b)); 255 | } 256 | 257 | return buffer.toString(); 258 | } else { 259 | return Long.toHexString(hash); 260 | } 261 | } 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /src/main/java/io/takari/aether/localrepo/TakariLocalRepositoryManager.java: -------------------------------------------------------------------------------- 1 | package io.takari.aether.localrepo; 2 | 3 | /******************************************************************************* 4 | * Copyright (c) 2010, 2013 Sonatype, Inc. 5 | * All rights reserved. This program and the accompanying materials 6 | * are made available under the terms of the Eclipse Public License v1.0 7 | * which accompanies this distribution, and is available at 8 | * http://www.eclipse.org/legal/epl-v10.html 9 | * 10 | * Contributors: 11 | * Sonatype, Inc. - initial API and implementation 12 | *******************************************************************************/ 13 | 14 | import java.io.File; 15 | import java.io.UnsupportedEncodingException; 16 | import java.security.MessageDigest; 17 | import java.security.NoSuchAlgorithmException; 18 | import java.util.Collection; 19 | import java.util.Collections; 20 | import java.util.HashMap; 21 | import java.util.HashSet; 22 | import java.util.List; 23 | import java.util.Map; 24 | import java.util.Properties; 25 | import java.util.SortedSet; 26 | import java.util.TreeSet; 27 | 28 | import org.eclipse.aether.RepositorySystemSession; 29 | import org.eclipse.aether.artifact.Artifact; 30 | import org.eclipse.aether.metadata.Metadata; 31 | import org.eclipse.aether.repository.LocalArtifactRegistration; 32 | import org.eclipse.aether.repository.LocalArtifactRequest; 33 | import org.eclipse.aether.repository.LocalArtifactResult; 34 | import org.eclipse.aether.repository.LocalMetadataRegistration; 35 | import org.eclipse.aether.repository.LocalMetadataRequest; 36 | import org.eclipse.aether.repository.LocalMetadataResult; 37 | import org.eclipse.aether.repository.LocalRepository; 38 | import org.eclipse.aether.repository.LocalRepositoryManager; 39 | import org.eclipse.aether.repository.RemoteRepository; 40 | 41 | /** 42 | * These are implementation details for enhanced local repository manager, subject to change without prior notice. 43 | * Repositories from which a cached artifact was resolved are tracked in a properties file named 44 | * _remote.repositories, with content key as filename>repo_id and value as empty string. If a file has 45 | * been installed in the repository, but not downloaded from a remote repository, it is tracked as empty repository id 46 | * and always resolved. For example: 47 | * 48 | *
 49 |  * artifact-1.0.pom>=
 50 |  * artifact-1.0.jar>=
 51 |  * artifact-1.0.pom>central=
 52 |  * artifact-1.0.jar>central=
 53 |  * artifact-1.0.zip>central=
 54 |  * artifact-1.0-classifier.zip>central=
 55 |  * artifact-1.0.pom>my_repo_id=
 56 |  * 
57 | * 58 | */ 59 | public class TakariLocalRepositoryManager implements LocalRepositoryManager { 60 | 61 | public static final String REPOSITORY_URI = ".repositoryUri"; 62 | private static final String LOCAL_REPO_ID = ""; 63 | private final String trackingFilename; 64 | private final TrackingFileManager trackingFileManager; 65 | private final LocalRepository localRepository; 66 | private final List validators; 67 | 68 | public TakariLocalRepositoryManager(File basedir, RepositorySystemSession session, List validators) { 69 | if (basedir == null) { 70 | throw new IllegalArgumentException("base directory has not been specified"); 71 | } 72 | this.validators = validators; 73 | localRepository = new LocalRepository(basedir.getAbsoluteFile(), "enhanced"); 74 | String filename = getString(session, "", "aether.enhancedLocalRepository.trackingFilename"); 75 | if (filename.length() <= 0 || filename.contains("/") || filename.contains("\\") || filename.contains("..")) { 76 | filename = "_remote.repositories"; 77 | } 78 | trackingFilename = filename; 79 | trackingFileManager = new TrackingFileManager(); 80 | } 81 | 82 | public LocalArtifactResult find(RepositorySystemSession session, LocalArtifactRequest request) { 83 | String path = getPathForArtifact(request.getArtifact(), false); 84 | File file = new File(getRepository().getBasedir(), path); 85 | LocalArtifactResult result = new LocalArtifactResult(request); 86 | if (file.isFile()) { 87 | result.setFile(file); 88 | Properties props = readRepos(file); 89 | if (props.get(getKey(file, LOCAL_REPO_ID)) != null) { 90 | // 91 | // artifact installed into the local repo is always accepted 92 | // 93 | result.setAvailable(true); 94 | } else { 95 | RemoteRepository remoteRepositoryForArtifact = null; 96 | String context = request.getContext(); 97 | for (RemoteRepository remoteRepository : request.getRepositories()) { 98 | if (props.get(getKey(file, getRepositoryKey(remoteRepository, context))) != null) { 99 | // 100 | // This is the remote repository that the artifact was resolved from initially. If the artifact is now available from 101 | // a different remote repository 102 | remoteRepositoryForArtifact = remoteRepository; 103 | result.setRepository(remoteRepositoryForArtifact); 104 | break; 105 | } 106 | } 107 | try { 108 | for (ArtifactValidator validator : validators) { 109 | validator.validateOnFind(request.getArtifact(), localRepository, 110 | remoteRepositoryForArtifact); 111 | } 112 | result.setFile(file); 113 | result.setAvailable(true); 114 | } catch (ArtifactUnavailableException e) { 115 | // sadly, there is no way to communicate the exception/message to the caller 116 | result.setFile(null); 117 | result.setAvailable(false); 118 | } 119 | 120 | /* 121 | 122 | This is the check to make sure what is found locally comes from the same remote repository. We actually don't care where it comes 123 | from really provided the SHA1 is the same. 124 | 125 | String context = request.getContext(); 126 | for (RemoteRepository remoteRepository : request.getRepositories()) { 127 | if (props.get(getKey(file, getRepositoryKey(remoteRepository, context))) != null) { 128 | // artifact downloaded from remote repository is accepted 129 | for (ArtifactValidator validator : validators) { 130 | validator.validateOnFind(request.getArtifact(), localRepository, remoteRepository); 131 | } 132 | result.setAvailable(true); 133 | result.setRepository(remoteRepository); 134 | break; 135 | } 136 | } 137 | if (!result.isAvailable() && !isTracked(props, file)) { 138 | // NOTE: The artifact is present but not tracked at all, for inter-op with simple local repo, assume the artifact was locally installed. 139 | result.setAvailable(true); 140 | } 141 | */ 142 | 143 | } 144 | } 145 | 146 | return result; 147 | } 148 | 149 | public void add(RepositorySystemSession session, LocalArtifactRegistration request) { 150 | Collection repositories; 151 | if (request.getRepository() == null) { 152 | repositories = Collections.singleton(LOCAL_REPO_ID); 153 | } else { 154 | repositories = getRepositoryKeys(request.getRepository(), request.getContexts()); 155 | } 156 | 157 | Artifact artifact = request.getArtifact(); 158 | boolean local = request.getRepository() == null; 159 | 160 | if (artifact == null) { 161 | throw new IllegalArgumentException("artifact to register not specified"); 162 | } 163 | String path = getPathForArtifact(artifact, local); 164 | File file = new File(getRepository().getBasedir(), path); 165 | 166 | Map updates = new HashMap(); 167 | for (String repository : repositories) { 168 | updates.put(getKey(file, repository), ""); 169 | } 170 | 171 | File trackingFile = getTrackingFile(file); 172 | trackingFileManager.update(trackingFile, updates); 173 | 174 | // The files are now present in the local repository 175 | for (ArtifactValidator validator : validators) { 176 | validator.validateOnAdd(request.getArtifact(), localRepository, request.getRepository()); 177 | } 178 | 179 | } 180 | 181 | private Collection getRepositoryKeys(RemoteRepository repository, Collection contexts) { 182 | Collection keys = new HashSet(); 183 | 184 | if (contexts != null) { 185 | for (String context : contexts) { 186 | keys.add(getRepositoryKey(repository, context)); 187 | } 188 | } 189 | 190 | return keys; 191 | } 192 | 193 | private Properties readRepos(File artifactFile) { 194 | File trackingFile = getTrackingFile(artifactFile); 195 | Properties props = trackingFileManager.read(trackingFile); 196 | return (props != null) ? props : new Properties(); 197 | } 198 | 199 | private File getTrackingFile(File artifactFile) { 200 | return new File(artifactFile.getParentFile(), trackingFilename); 201 | } 202 | 203 | private String getKey(File file, String repository) { 204 | return file.getName() + '>' + repository; 205 | } 206 | 207 | public static String getString(Map properties, String defaultValue, String... keys) { 208 | for (String key : keys) { 209 | Object value = properties.get(key); 210 | 211 | if (value instanceof String) { 212 | return (String) value; 213 | } 214 | } 215 | 216 | return defaultValue; 217 | } 218 | 219 | public static String getString(RepositorySystemSession session, String defaultValue, String... keys) { 220 | return getString(session.getConfigProperties(), defaultValue, keys); 221 | } 222 | 223 | /////// 224 | 225 | public LocalRepository getRepository() { 226 | return localRepository; 227 | } 228 | 229 | String getPathForArtifact(Artifact artifact, boolean local) { 230 | StringBuilder path = new StringBuilder(128); 231 | path.append(artifact.getGroupId().replace('.', '/')).append('/'); 232 | path.append(artifact.getArtifactId()).append('/'); 233 | path.append(artifact.getBaseVersion()).append('/'); 234 | path.append(artifact.getArtifactId()).append('-'); 235 | if (local) { 236 | path.append(artifact.getBaseVersion()); 237 | } else { 238 | path.append(artifact.getVersion()); 239 | } 240 | if (artifact.getClassifier().length() > 0) { 241 | path.append('-').append(artifact.getClassifier()); 242 | } 243 | if (artifact.getExtension().length() > 0) { 244 | path.append('.').append(artifact.getExtension()); 245 | } 246 | return path.toString(); 247 | } 248 | 249 | public String getPathForLocalArtifact(Artifact artifact) { 250 | return getPathForArtifact(artifact, true); 251 | } 252 | 253 | public String getPathForRemoteArtifact(Artifact artifact, RemoteRepository repository, String context) { 254 | return getPathForArtifact(artifact, false); 255 | } 256 | 257 | public String getPathForLocalMetadata(Metadata metadata) { 258 | return getPath(metadata, "local"); 259 | } 260 | 261 | public String getPathForRemoteMetadata(Metadata metadata, RemoteRepository repository, String context) { 262 | return getPath(metadata, getRepositoryKey(repository, context)); 263 | } 264 | 265 | String getRepositoryKey(RemoteRepository repository, String context) { 266 | String key; 267 | if (repository.isRepositoryManager()) { 268 | // repository serves dynamic contents, take request parameters into account for key 269 | StringBuilder buffer = new StringBuilder(128); 270 | buffer.append(repository.getId()); 271 | buffer.append('-'); 272 | 273 | SortedSet subKeys = new TreeSet(); 274 | for (RemoteRepository mirroredRepo : repository.getMirroredRepositories()) { 275 | subKeys.add(mirroredRepo.getId()); 276 | } 277 | 278 | SimpleDigest digest = new SimpleDigest(); 279 | digest.update(context); 280 | for (String subKey : subKeys) { 281 | digest.update(subKey); 282 | } 283 | buffer.append(digest.digest()); 284 | 285 | key = buffer.toString(); 286 | } else { 287 | // repository serves static contents, its id is sufficient as key 288 | key = repository.getId(); 289 | } 290 | 291 | return key; 292 | } 293 | 294 | private String getPath(Metadata metadata, String repositoryKey) { 295 | StringBuilder path = new StringBuilder(128); 296 | 297 | if (metadata.getGroupId().length() > 0) { 298 | path.append(metadata.getGroupId().replace('.', '/')).append('/'); 299 | 300 | if (metadata.getArtifactId().length() > 0) { 301 | path.append(metadata.getArtifactId()).append('/'); 302 | 303 | if (metadata.getVersion().length() > 0) { 304 | path.append(metadata.getVersion()).append('/'); 305 | } 306 | } 307 | } 308 | 309 | path.append(insertRepositoryKey(metadata.getType(), repositoryKey)); 310 | 311 | return path.toString(); 312 | } 313 | 314 | private String insertRepositoryKey(String filename, String repositoryKey) { 315 | String result; 316 | int idx = filename.indexOf('.'); 317 | if (idx < 0) { 318 | result = filename + '-' + repositoryKey; 319 | } else { 320 | result = filename.substring(0, idx) + '-' + repositoryKey + filename.substring(idx); 321 | } 322 | return result; 323 | } 324 | 325 | public LocalArtifactResult Xfind(RepositorySystemSession session, LocalArtifactRequest request) { 326 | String path = getPathForArtifact(request.getArtifact(), false); 327 | File file = new File(getRepository().getBasedir(), path); 328 | 329 | LocalArtifactResult result = new LocalArtifactResult(request); 330 | if (file.isFile()) { 331 | result.setFile(file); 332 | result.setAvailable(true); 333 | } 334 | 335 | return result; 336 | } 337 | 338 | public void Xadd(RepositorySystemSession session, LocalArtifactRegistration request) { 339 | // noop 340 | } 341 | 342 | @Override 343 | public String toString() { 344 | return String.valueOf(getRepository()); 345 | } 346 | 347 | public LocalMetadataResult find(RepositorySystemSession session, LocalMetadataRequest request) { 348 | LocalMetadataResult result = new LocalMetadataResult(request); 349 | 350 | String path; 351 | 352 | Metadata metadata = request.getMetadata(); 353 | String context = request.getContext(); 354 | RemoteRepository remote = request.getRepository(); 355 | 356 | if (remote != null) { 357 | path = getPathForRemoteMetadata(metadata, remote, context); 358 | } else { 359 | path = getPathForLocalMetadata(metadata); 360 | } 361 | 362 | File file = new File(getRepository().getBasedir(), path); 363 | if (file.isFile()) { 364 | result.setFile(file); 365 | } 366 | 367 | return result; 368 | } 369 | 370 | public void add(RepositorySystemSession session, LocalMetadataRegistration request) { 371 | // noop 372 | } 373 | 374 | // 375 | // Just use Guava 376 | // 377 | class SimpleDigest { 378 | 379 | private MessageDigest digest; 380 | 381 | private long hash; 382 | 383 | public SimpleDigest() { 384 | try { 385 | digest = MessageDigest.getInstance("SHA-1"); 386 | } catch (NoSuchAlgorithmException e) { 387 | try { 388 | digest = MessageDigest.getInstance("MD5"); 389 | } catch (NoSuchAlgorithmException ne) { 390 | digest = null; 391 | hash = 13; 392 | } 393 | } 394 | } 395 | 396 | public void update(String data) { 397 | if (data == null || data.length() <= 0) { 398 | return; 399 | } 400 | if (digest != null) { 401 | try { 402 | digest.update(data.getBytes("UTF-8")); 403 | } catch (UnsupportedEncodingException e) { 404 | // broken JVM 405 | } 406 | } else { 407 | hash = hash * 31 + data.hashCode(); 408 | } 409 | } 410 | 411 | public String digest() { 412 | if (digest != null) { 413 | StringBuilder buffer = new StringBuilder(64); 414 | 415 | byte[] bytes = digest.digest(); 416 | for (int i = 0; i < bytes.length; i++) { 417 | int b = bytes[i] & 0xFF; 418 | 419 | if (b < 0x10) { 420 | buffer.append('0'); 421 | } 422 | 423 | buffer.append(Integer.toHexString(b)); 424 | } 425 | 426 | return buffer.toString(); 427 | } else { 428 | return Long.toHexString(hash); 429 | } 430 | } 431 | } 432 | } 433 | -------------------------------------------------------------------------------- /src/main/java/io/takari/aether/localrepo/TakariLocalRepositoryManagerFactory.java: -------------------------------------------------------------------------------- 1 | package io.takari.aether.localrepo; 2 | 3 | /******************************************************************************* 4 | * Copyright (c) 2010, 2013 Sonatype, Inc. 5 | * All rights reserved. This program and the accompanying materials 6 | * are made available under the terms of the Eclipse Public License v1.0 7 | * which accompanies this distribution, and is available at 8 | * http://www.eclipse.org/legal/epl-v10.html 9 | * 10 | * Contributors: 11 | * Sonatype, Inc. - initial API and implementation 12 | *******************************************************************************/ 13 | 14 | import java.util.List; 15 | 16 | import javax.inject.Inject; 17 | import javax.inject.Named; 18 | 19 | import org.eclipse.aether.RepositorySystemSession; 20 | import org.eclipse.aether.repository.LocalRepository; 21 | import org.eclipse.aether.repository.LocalRepositoryManager; 22 | import org.eclipse.aether.repository.NoLocalRepositoryManagerException; 23 | import org.eclipse.aether.spi.localrepo.LocalRepositoryManagerFactory; 24 | 25 | import com.google.common.collect.Lists; 26 | 27 | /** 28 | * Creates enhanced local repository managers for repository types {@code "default"} or {@code "" (automatic)}. 29 | * Enhanced local repository manager is built upon the classical Maven 2.0 local repository structure but additionally keeps 30 | * track of from what repositories a cached artifact was resolved. 31 | * Resolution of locally cached artifacts will be rejected in case the current resolution request does not match the 32 | * known source repositories of an artifact, thereby emulating physically separated artifact caches per remote repository. 33 | */ 34 | @Named("takari") 35 | public class TakariLocalRepositoryManagerFactory implements LocalRepositoryManagerFactory { 36 | 37 | @Inject 38 | List validators; 39 | 40 | @Override 41 | public LocalRepositoryManager newInstance(RepositorySystemSession session, LocalRepository repository) throws NoLocalRepositoryManagerException { 42 | if ("".equals(repository.getContentType()) || "default".equals(repository.getContentType())) { 43 | if(validators != null) { 44 | return new TakariLocalRepositoryManager(repository.getBasedir(), session, validators); 45 | } else { 46 | return new TakariLocalRepositoryManager(repository.getBasedir(), session, Lists.newArrayList()); 47 | } 48 | } else { 49 | throw new NoLocalRepositoryManagerException(repository); 50 | } 51 | } 52 | 53 | @Override 54 | public float getPriority() { 55 | return 20; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/io/takari/aether/localrepo/TakariUpdateCheckManager.java: -------------------------------------------------------------------------------- 1 | package io.takari.aether.localrepo; 2 | 3 | /******************************************************************************* 4 | * Copyright (c) 2010, 2011 Sonatype, Inc. 5 | * All rights reserved. This program and the accompanying materials 6 | * are made available under the terms of the Eclipse Public License v1.0 7 | * which accompanies this distribution, and is available at 8 | * http://www.eclipse.org/legal/epl-v10.html 9 | * 10 | * Contributors: 11 | * Sonatype, Inc. - initial API and implementation 12 | *******************************************************************************/ 13 | 14 | import java.io.File; 15 | import java.util.Collections; 16 | import java.util.HashMap; 17 | import java.util.Map; 18 | import java.util.Properties; 19 | import java.util.Set; 20 | import java.util.TreeSet; 21 | import java.util.concurrent.ConcurrentHashMap; 22 | 23 | import javax.inject.Inject; 24 | import javax.inject.Named; 25 | import javax.inject.Singleton; 26 | 27 | import org.eclipse.aether.RepositorySystemSession; 28 | import org.eclipse.aether.SessionData; 29 | import org.eclipse.aether.artifact.Artifact; 30 | import org.eclipse.aether.impl.UpdateCheck; 31 | import org.eclipse.aether.impl.UpdateCheckManager; 32 | import org.eclipse.aether.impl.UpdatePolicyAnalyzer; 33 | import org.eclipse.aether.metadata.Metadata; 34 | import org.eclipse.aether.repository.AuthenticationDigest; 35 | import org.eclipse.aether.repository.Proxy; 36 | import org.eclipse.aether.repository.RemoteRepository; 37 | import org.eclipse.aether.resolution.ResolutionErrorPolicy; 38 | import org.eclipse.aether.resolution.ResolutionErrorPolicyRequest; 39 | import org.eclipse.aether.transfer.ArtifactNotFoundException; 40 | import org.eclipse.aether.transfer.ArtifactTransferException; 41 | import org.eclipse.aether.transfer.MetadataNotFoundException; 42 | import org.eclipse.aether.transfer.MetadataTransferException; 43 | import org.slf4j.Logger; 44 | import org.slf4j.LoggerFactory; 45 | 46 | @Named("takari") 47 | @Singleton 48 | public class TakariUpdateCheckManager implements UpdateCheckManager { 49 | 50 | private Logger logger = LoggerFactory.getLogger(TakariUpdateCheckManager.class); 51 | 52 | private static final String ERROR_FLAG = "maven.retryOnDownloadError"; 53 | 54 | @Inject 55 | private UpdatePolicyAnalyzer updatePolicyAnalyzer; 56 | 57 | private static final String UPDATED_KEY_SUFFIX = ".lastUpdated"; 58 | 59 | private static final String ERROR_KEY_SUFFIX = ".error"; 60 | 61 | private static final String NOT_FOUND = ""; 62 | 63 | private static final String SESSION_CHECKS = "updateCheckManager.checks"; 64 | 65 | private final boolean allowImmediateRetryOfDownloadFailures; 66 | 67 | public TakariUpdateCheckManager() { 68 | if (System.getProperty(ERROR_FLAG) != null) { 69 | this.allowImmediateRetryOfDownloadFailures = Boolean.getBoolean(ERROR_FLAG); 70 | } else { 71 | this.allowImmediateRetryOfDownloadFailures = false; 72 | } 73 | } 74 | 75 | public UpdateCheckManager setUpdatePolicyAnalyzer(UpdatePolicyAnalyzer updatePolicyAnalyzer) { 76 | this.updatePolicyAnalyzer = updatePolicyAnalyzer; 77 | return this; 78 | } 79 | 80 | 81 | @Override 82 | public void checkArtifact(RepositorySystemSession session, UpdateCheck check) { 83 | if (check.getLocalLastUpdated() != 0 && !isUpdatedRequired(session, check.getLocalLastUpdated(), check.getPolicy())) { 84 | if (logger.isDebugEnabled()) { 85 | logger.debug("Skipped remote request for " + check.getItem() + ", locally installed artifact up-to-date."); 86 | } 87 | 88 | check.setRequired(false); 89 | return; 90 | } 91 | 92 | Artifact artifact = check.getItem(); 93 | RemoteRepository repository = check.getRepository(); 94 | 95 | File artifactFile = check.getFile(); 96 | if (artifactFile == null) { 97 | throw new IllegalArgumentException(String.format("The artifact '%s' has no file attached", artifact)); 98 | } 99 | 100 | boolean fileExists = check.isFileValid() && artifactFile.exists(); 101 | 102 | File touchFile = getTouchFile(artifact, artifactFile); 103 | Properties props = read(touchFile); 104 | 105 | String updateKey = getUpdateKey(session, artifactFile, repository); 106 | String dataKey = getDataKey(artifact, artifactFile, repository); 107 | 108 | String error = getError(props, dataKey); 109 | 110 | long lastUpdated; 111 | if (error == null) { 112 | if (fileExists) { 113 | // last update was successful 114 | lastUpdated = artifactFile.lastModified(); 115 | } else { 116 | // this is the first attempt ever 117 | lastUpdated = 0; 118 | } 119 | } else if (error.length() <= 0) { 120 | // artifact did not exist 121 | lastUpdated = getLastUpdated(props, dataKey); 122 | } else { 123 | // artifact could not be transferred 124 | String transferKey = getTransferKey(session, artifact, artifactFile, repository); 125 | lastUpdated = getLastUpdated(props, transferKey); 126 | } 127 | 128 | if (lastUpdated == 0) { 129 | check.setRequired(true); 130 | } else if (isAlreadyUpdated(session.getData(), updateKey)) { 131 | if (logger.isDebugEnabled()) { 132 | logger.debug("Skipped remote request for " + check.getItem() + ", already updated during this session."); 133 | } 134 | 135 | check.setRequired(false); 136 | if (error != null) { 137 | check.setException(newException(error, artifact, repository)); 138 | } 139 | } else if (isUpdatedRequired(session, lastUpdated, check.getPolicy())) { 140 | check.setRequired(true); 141 | } else if (fileExists) { 142 | if (logger.isDebugEnabled()) { 143 | logger.debug("Skipped remote request for " + check.getItem() + ", locally cached artifact up-to-date."); 144 | } 145 | 146 | check.setRequired(false); 147 | } else { 148 | int errorPolicy = getPolicy(session, artifact, repository); 149 | if (error == null || error.length() <= 0) { 150 | if ((errorPolicy & ResolutionErrorPolicy.CACHE_NOT_FOUND) != 0) { 151 | check.setRequired(false); 152 | check.setException(newException(error, artifact, repository)); 153 | } else { 154 | check.setRequired(true); 155 | } 156 | } else { 157 | if ((errorPolicy & ResolutionErrorPolicy.CACHE_TRANSFER_ERROR) != 0) { 158 | check.setRequired(false); 159 | check.setException(newException(error, artifact, repository)); 160 | } else { 161 | check.setRequired(true); 162 | } 163 | } 164 | } 165 | } 166 | 167 | private ArtifactTransferException newException(String error, Artifact artifact, RemoteRepository repository) { 168 | if (error == null || error.length() <= 0) { 169 | return new ArtifactNotFoundException(artifact, repository, "Failure to find " + artifact + " in " + repository.getUrl() + " was cached in the local repository, " 170 | + "resolution will not be reattempted until the update interval of " + repository.getId() + " has elapsed or updates are forced"); 171 | } else { 172 | return new ArtifactTransferException(artifact, repository, "Failure to transfer " + artifact + " from " + repository.getUrl() + " was cached in the local repository, " 173 | + "resolution will not be reattempted until the update interval of " + repository.getId() + " has elapsed or updates are forced. Original error: " + error); 174 | } 175 | } 176 | 177 | @Override 178 | public void checkMetadata(RepositorySystemSession session, UpdateCheck check) { 179 | if (check.getLocalLastUpdated() != 0 && !isUpdatedRequired(session, check.getLocalLastUpdated(), check.getPolicy())) { 180 | if (logger.isDebugEnabled()) { 181 | logger.debug("Skipped remote request for " + check.getItem() + ", locally installed metadata up-to-date."); 182 | } 183 | 184 | check.setRequired(false); 185 | return; 186 | } 187 | 188 | Metadata metadata = check.getItem(); 189 | RemoteRepository repository = check.getRepository(); 190 | 191 | File metadataFile = check.getFile(); 192 | if (metadataFile == null) { 193 | throw new IllegalArgumentException(String.format("The metadata '%s' has no file attached", metadata)); 194 | } 195 | 196 | boolean fileExists = check.isFileValid() && metadataFile.exists(); 197 | 198 | File touchFile = getTouchFile(metadata, metadataFile); 199 | Properties props = read(touchFile); 200 | 201 | String updateKey = getUpdateKey(session, metadataFile, repository); 202 | String dataKey = getDataKey(metadata, metadataFile, check.getAuthoritativeRepository()); 203 | 204 | String error = getError(props, dataKey); 205 | 206 | long lastUpdated; 207 | if (error == null) { 208 | if (fileExists) { 209 | // last update was successful 210 | lastUpdated = getLastUpdated(props, dataKey); 211 | } else { 212 | // this is the first attempt ever 213 | lastUpdated = 0; 214 | } 215 | } else if (error.length() <= 0) { 216 | // metadata did not exist 217 | lastUpdated = getLastUpdated(props, dataKey); 218 | } else { 219 | // metadata could not be transferred 220 | String transferKey = getTransferKey(session, metadata, metadataFile, repository); 221 | lastUpdated = getLastUpdated(props, transferKey); 222 | } 223 | 224 | if (lastUpdated == 0) { 225 | check.setRequired(true); 226 | } else if (isAlreadyUpdated(session.getData(), updateKey)) { 227 | if (logger.isDebugEnabled()) { 228 | logger.debug("Skipped remote request for " + check.getItem() + ", already updated during this session."); 229 | } 230 | 231 | check.setRequired(false); 232 | if (error != null) { 233 | check.setException(newException(error, metadata, repository)); 234 | } 235 | } else if (isUpdatedRequired(session, lastUpdated, check.getPolicy())) { 236 | check.setRequired(true); 237 | } else if (fileExists) { 238 | if (logger.isDebugEnabled()) { 239 | logger.debug("Skipped remote request for " + check.getItem() + ", locally cached metadata up-to-date."); 240 | } 241 | 242 | check.setRequired(false); 243 | } else { 244 | int errorPolicy = getPolicy(session, metadata, repository); 245 | if (error == null || error.length() <= 0) { 246 | if ((errorPolicy & ResolutionErrorPolicy.CACHE_NOT_FOUND) != 0) { 247 | check.setRequired(false); 248 | check.setException(newException(error, metadata, repository)); 249 | } else { 250 | check.setRequired(true); 251 | } 252 | } else { 253 | if ((errorPolicy & ResolutionErrorPolicy.CACHE_TRANSFER_ERROR) != 0) { 254 | check.setRequired(false); 255 | check.setException(newException(error, metadata, repository)); 256 | } else { 257 | check.setRequired(true); 258 | } 259 | } 260 | } 261 | } 262 | 263 | private MetadataTransferException newException(String error, Metadata metadata, RemoteRepository repository) { 264 | if (error == null || error.length() <= 0) { 265 | return new MetadataNotFoundException(metadata, repository, "Failure to find " + metadata + " in " + repository.getUrl() + " was cached in the local repository, " 266 | + "resolution will not be reattempted until the update interval of " + repository.getId() + " has elapsed or updates are forced"); 267 | } else { 268 | return new MetadataTransferException(metadata, repository, "Failure to transfer " + metadata + " from " + repository.getUrl() + " was cached in the local repository, " 269 | + "resolution will not be reattempted until the update interval of " + repository.getId() + " has elapsed or updates are forced. Original error: " + error); 270 | } 271 | } 272 | 273 | private long getLastUpdated(Properties props, String key) { 274 | String value = props.getProperty(key + UPDATED_KEY_SUFFIX, ""); 275 | try { 276 | return (value.length() > 0) ? Long.parseLong(value) : 1; 277 | } catch (NumberFormatException e) { 278 | logger.debug("Cannot parse lastUpdated date: \'" + value + "\'. Ignoring.", e); 279 | return 1; 280 | } 281 | } 282 | 283 | private String getError(Properties props, String key) { 284 | return props.getProperty(key + ERROR_KEY_SUFFIX); 285 | } 286 | 287 | private File getTouchFile(Artifact artifact, File artifactFile) { 288 | return new File(artifactFile.getPath() + ".lastUpdated"); 289 | } 290 | 291 | private File getTouchFile(Metadata metadata, File metadataFile) { 292 | return new File(metadataFile.getParent(), "resolver-status.properties"); 293 | } 294 | 295 | private String getDataKey(Artifact artifact, File artifactFile, RemoteRepository repository) { 296 | Set mirroredUrls = Collections.emptySet(); 297 | if (repository.isRepositoryManager()) { 298 | mirroredUrls = new TreeSet(); 299 | for (RemoteRepository mirroredRepository : repository.getMirroredRepositories()) { 300 | mirroredUrls.add(normalizeRepoUrl(mirroredRepository.getUrl())); 301 | } 302 | } 303 | 304 | StringBuilder buffer = new StringBuilder(1024); 305 | 306 | buffer.append(normalizeRepoUrl(repository.getUrl())); 307 | for (String mirroredUrl : mirroredUrls) { 308 | buffer.append('+').append(mirroredUrl); 309 | } 310 | 311 | return buffer.toString(); 312 | } 313 | 314 | private String getTransferKey(RepositorySystemSession session, Artifact artifact, File artifactFile, RemoteRepository repository) { 315 | return getRepoKey(session, repository); 316 | } 317 | 318 | private String getDataKey(Metadata metadata, File metadataFile, RemoteRepository repository) { 319 | return metadataFile.getName(); 320 | } 321 | 322 | private String getTransferKey(RepositorySystemSession session, Metadata metadata, File metadataFile, RemoteRepository repository) { 323 | return metadataFile.getName() + '/' + getRepoKey(session, repository); 324 | } 325 | 326 | private String getRepoKey(RepositorySystemSession session, RemoteRepository repository) { 327 | StringBuilder buffer = new StringBuilder(128); 328 | 329 | Proxy proxy = repository.getProxy(); 330 | if (proxy != null) { 331 | buffer.append(AuthenticationDigest.forProxy(session, repository)).append('@'); 332 | buffer.append(proxy.getHost()).append(':').append(proxy.getPort()).append('>'); 333 | } 334 | 335 | buffer.append(AuthenticationDigest.forRepository(session, repository)).append('@'); 336 | 337 | buffer.append(repository.getContentType()).append('-'); 338 | buffer.append(repository.getId() ).append( '-' ); 339 | buffer.append(normalizeRepoUrl(repository.getUrl())); 340 | 341 | return buffer.toString(); 342 | } 343 | 344 | private String normalizeRepoUrl(String url) { 345 | String result = url; 346 | if (url != null && url.length() > 0 && !url.endsWith("/")) { 347 | result = url + '/'; 348 | } 349 | return result; 350 | } 351 | 352 | private String getUpdateKey(RepositorySystemSession session, File file, RemoteRepository repository) { 353 | return file.getAbsolutePath() + '|' + getRepoKey(session, repository); 354 | } 355 | 356 | private boolean isAlreadyUpdated(SessionData data, Object updateKey) { 357 | Object checkedFiles = data.get(SESSION_CHECKS); 358 | if (!(checkedFiles instanceof Map)) { 359 | return false; 360 | } 361 | return ((Map) checkedFiles).containsKey(updateKey); 362 | } 363 | 364 | @SuppressWarnings("unchecked") 365 | private void setUpdated(SessionData data, Object updateKey) { 366 | Object checkedFiles = data.get(SESSION_CHECKS); 367 | while (!(checkedFiles instanceof Map)) { 368 | Object old = checkedFiles; 369 | checkedFiles = new ConcurrentHashMap(256); 370 | if (data.set(SESSION_CHECKS, old, checkedFiles)) { 371 | break; 372 | } 373 | checkedFiles = data.get(SESSION_CHECKS); 374 | } 375 | ((Map) checkedFiles).put(updateKey, Boolean.TRUE); 376 | } 377 | 378 | private boolean isUpdatedRequired(RepositorySystemSession session, long lastModified, String policy) { 379 | return updatePolicyAnalyzer.isUpdatedRequired(session, lastModified, policy); 380 | } 381 | 382 | private Properties read(File touchFile) { 383 | Properties props = new TrackingFileManager().read(touchFile); 384 | return (props != null) ? props : new Properties(); 385 | } 386 | 387 | @Override 388 | public void touchArtifact(RepositorySystemSession session, UpdateCheck check) { 389 | Artifact artifact = check.getItem(); 390 | File artifactFile = check.getFile(); 391 | File touchFile = getTouchFile(artifact, artifactFile); 392 | 393 | String updateKey = getUpdateKey(session, artifactFile, check.getRepository()); 394 | String dataKey = getDataKey(artifact, artifactFile, check.getAuthoritativeRepository()); 395 | String transferKey = getTransferKey(session, artifact, artifactFile, check.getRepository()); 396 | 397 | setUpdated(session.getData(), updateKey); 398 | Properties props = write(touchFile, dataKey, transferKey, check.getException()); 399 | 400 | if (artifactFile.exists() && !hasErrors(props)) { 401 | touchFile.delete(); 402 | } 403 | } 404 | 405 | private boolean hasErrors(Properties props) { 406 | for (Object key : props.keySet()) { 407 | if (key.toString().endsWith(ERROR_KEY_SUFFIX)) { 408 | return true; 409 | } 410 | } 411 | return false; 412 | } 413 | 414 | @Override 415 | public void touchMetadata(RepositorySystemSession session, UpdateCheck check) { 416 | Metadata metadata = check.getItem(); 417 | File metadataFile = check.getFile(); 418 | File touchFile = getTouchFile(metadata, metadataFile); 419 | 420 | String updateKey = getUpdateKey(session, metadataFile, check.getRepository()); 421 | String dataKey = getDataKey(metadata, metadataFile, check.getAuthoritativeRepository()); 422 | String transferKey = getTransferKey(session, metadata, metadataFile, check.getRepository()); 423 | 424 | setUpdated(session.getData(), updateKey); 425 | write(touchFile, dataKey, transferKey, check.getException()); 426 | } 427 | 428 | private Properties write(File touchFile, String dataKey, String transferKey, Exception error) { 429 | Map updates = new HashMap(); 430 | 431 | String timestamp = Long.toString(System.currentTimeMillis()); 432 | 433 | if (error == null) { 434 | updates.put(dataKey + ERROR_KEY_SUFFIX, null); 435 | updates.put(dataKey + UPDATED_KEY_SUFFIX, timestamp); 436 | updates.put(transferKey + UPDATED_KEY_SUFFIX, null); 437 | } else if (error instanceof ArtifactNotFoundException || error instanceof MetadataNotFoundException) { 438 | updates.put(dataKey + ERROR_KEY_SUFFIX, NOT_FOUND); 439 | updates.put(dataKey + UPDATED_KEY_SUFFIX, timestamp); 440 | updates.put(transferKey + UPDATED_KEY_SUFFIX, null); 441 | } else { 442 | String msg = error.getMessage(); 443 | if (msg == null || msg.length() <= 0) { 444 | msg = error.getClass().getSimpleName(); 445 | } 446 | updates.put(dataKey + ERROR_KEY_SUFFIX, msg); 447 | updates.put(dataKey + UPDATED_KEY_SUFFIX, null); 448 | updates.put(transferKey + UPDATED_KEY_SUFFIX, timestamp); 449 | } 450 | 451 | if (allowImmediateRetryOfDownloadFailures) { 452 | // Don't track the error so that the next time Maven fires up it will retry to download 453 | return new Properties(); 454 | } else { 455 | return new TrackingFileManager().update(touchFile, updates); 456 | } 457 | } 458 | 459 | private static int getPolicy(RepositorySystemSession session, Artifact artifact, RemoteRepository repository) { 460 | ResolutionErrorPolicy rep = session.getResolutionErrorPolicy(); 461 | if (rep == null) { 462 | return ResolutionErrorPolicy.CACHE_DISABLED; 463 | } 464 | return rep.getArtifactPolicy(session, new ResolutionErrorPolicyRequest(artifact, repository)); 465 | } 466 | 467 | private static int getPolicy(RepositorySystemSession session, Metadata metadata, RemoteRepository repository) { 468 | ResolutionErrorPolicy rep = session.getResolutionErrorPolicy(); 469 | if (rep == null) { 470 | return ResolutionErrorPolicy.CACHE_DISABLED; 471 | } 472 | return rep.getMetadataPolicy(session, new ResolutionErrorPolicyRequest(metadata, repository)); 473 | } 474 | } 475 | -------------------------------------------------------------------------------- /src/main/java/io/takari/aether/localrepo/TrackingFileManager.java: -------------------------------------------------------------------------------- 1 | package io.takari.aether.localrepo; 2 | 3 | /******************************************************************************* 4 | * Copyright (c) 2010, 2011 Sonatype, Inc. 5 | * All rights reserved. This program and the accompanying materials 6 | * are made available under the terms of the Eclipse Public License v1.0 7 | * which accompanies this distribution, and is available at 8 | * http://www.eclipse.org/legal/epl-v10.html 9 | * 10 | * Contributors: 11 | * Sonatype, Inc. - initial API and implementation 12 | *******************************************************************************/ 13 | 14 | import java.io.ByteArrayInputStream; 15 | import java.io.ByteArrayOutputStream; 16 | import java.io.Closeable; 17 | import java.io.File; 18 | import java.io.FileInputStream; 19 | import java.io.IOException; 20 | import java.io.RandomAccessFile; 21 | import java.nio.channels.FileChannel; 22 | import java.nio.channels.FileLock; 23 | import java.nio.channels.OverlappingFileLockException; 24 | import java.util.Map; 25 | import java.util.Properties; 26 | 27 | import org.slf4j.Logger; 28 | import org.slf4j.LoggerFactory; 29 | 30 | /** 31 | * Manages potentially concurrent accesses to a properties file. 32 | */ 33 | public class TrackingFileManager { 34 | 35 | private Logger logger = LoggerFactory.getLogger( TrackingFileManager.class ); 36 | 37 | public Properties read(File file) { 38 | synchronized (getLock(file)) { 39 | FileLock lock = null; 40 | FileInputStream stream = null; 41 | try { 42 | if (!file.exists()) { 43 | return null; 44 | } 45 | 46 | stream = new FileInputStream(file); 47 | 48 | lock = lock(stream.getChannel(), Math.max(1, file.length()), true); 49 | 50 | Properties props = new Properties(); 51 | props.load(stream); 52 | 53 | return props; 54 | } catch (IOException e) { 55 | logger.warn("Failed to read tracking file " + file, e); 56 | } finally { 57 | release(lock, file); 58 | close(stream, file); 59 | } 60 | } 61 | 62 | return null; 63 | } 64 | 65 | public Properties update(File file, Map updates) { 66 | Properties props = new Properties(); 67 | 68 | synchronized (getLock(file)) { 69 | File directory = file.getParentFile(); 70 | if (!directory.mkdirs() && !directory.exists()) { 71 | logger.warn("Failed to create parent directories for tracking file " + file); 72 | return props; 73 | } 74 | 75 | RandomAccessFile raf = null; 76 | FileLock lock = null; 77 | try { 78 | raf = new RandomAccessFile(file, "rw"); 79 | lock = lock(raf.getChannel(), Math.max(1, raf.length()), false); 80 | 81 | if (file.canRead()) { 82 | byte[] buffer = new byte[(int) raf.length()]; 83 | 84 | raf.readFully(buffer); 85 | 86 | ByteArrayInputStream stream = new ByteArrayInputStream(buffer); 87 | 88 | props.load(stream); 89 | } 90 | 91 | for (Map.Entry update : updates.entrySet()) { 92 | if (update.getValue() == null) { 93 | props.remove(update.getKey()); 94 | } else { 95 | props.setProperty(update.getKey(), update.getValue()); 96 | } 97 | } 98 | 99 | ByteArrayOutputStream stream = new ByteArrayOutputStream(1024 * 2); 100 | 101 | logger.debug("Writing tracking file " + file); 102 | props.store(stream, "NOTE: This is an Aether internal implementation file" + ", its format can be changed without prior notice."); 103 | raf.seek(0); 104 | raf.write(stream.toByteArray()); 105 | raf.setLength(raf.getFilePointer()); 106 | } catch (IOException e) { 107 | logger.warn("Failed to write tracking file " + file, e); 108 | } finally { 109 | release(lock, file); 110 | close(raf, file); 111 | } 112 | } 113 | 114 | return props; 115 | } 116 | 117 | private void release(FileLock lock, File file) { 118 | if (lock != null) { 119 | try { 120 | lock.release(); 121 | } catch (IOException e) { 122 | logger.warn("Error releasing lock for tracking file " + file, e); 123 | } 124 | } 125 | } 126 | 127 | private void close(Closeable closeable, File file) { 128 | if (closeable != null) { 129 | try { 130 | closeable.close(); 131 | } catch (IOException e) { 132 | logger.warn("Error closing tracking file " + file, e); 133 | } 134 | } 135 | } 136 | 137 | private Object getLock(File file) { 138 | /* 139 | * NOTE: Locks held by one JVM must not overlap and using the canonical path is our best bet, still another piece of code might have locked the same file (unlikely though) or the canonical path 140 | * fails to capture file identity sufficiently as is the case with Java 1.6 and symlinks on Windows. 141 | */ 142 | try { 143 | return file.getCanonicalPath().intern(); 144 | } catch (IOException e) { 145 | logger.warn("Failed to canonicalize path " + file + ": " + e.getMessage()); 146 | return file.getAbsolutePath().intern(); 147 | } 148 | } 149 | 150 | private FileLock lock(FileChannel channel, long size, boolean shared) throws IOException { 151 | FileLock lock = null; 152 | 153 | for (int attempts = 8; attempts >= 0; attempts--) { 154 | try { 155 | lock = channel.lock(0, size, shared); 156 | break; 157 | } catch (OverlappingFileLockException e) { 158 | if (attempts <= 0) { 159 | throw (IOException) new IOException().initCause(e); 160 | } 161 | try { 162 | Thread.sleep(50); 163 | } catch (InterruptedException e1) { 164 | Thread.currentThread().interrupt(); 165 | } 166 | } 167 | } 168 | 169 | if (lock == null) { 170 | throw new IOException("Could not lock file"); 171 | } 172 | 173 | return lock; 174 | } 175 | 176 | } 177 | -------------------------------------------------------------------------------- /src/main/java/io/takari/filemanager/FileManager.java: -------------------------------------------------------------------------------- 1 | package io.takari.filemanager; 2 | 3 | /******************************************************************************* 4 | * Copyright (c) 2010-2013 Sonatype, Inc. 5 | * All rights reserved. This program and the accompanying materials 6 | * are made available under the terms of the Eclipse Public License v1.0 7 | * which accompanies this distribution, and is available at 8 | * http://www.eclipse.org/legal/epl-v10.html 9 | *******************************************************************************/ 10 | 11 | import io.takari.filemanager.FileManager.ProgressListener; 12 | 13 | import java.io.File; 14 | import java.io.IOException; 15 | import java.io.InputStream; 16 | import java.nio.ByteBuffer; 17 | 18 | /** 19 | * A LockManager holding external locks, locking files between OS processes (e.g. via {@link Lock}. 20 | * 21 | * @author Benjamin Hanzelmann 22 | */ 23 | public interface FileManager { 24 | 25 | /** 26 | * Obtain a lock object that may be used to lock the target file for reading. This method must not lock that file 27 | * right immediately (see {@link Lock#lock()}). 28 | * 29 | * @param target the file to lock, never {@code null}. 30 | * @return a lock object, never {@code null}. 31 | */ 32 | Lock readLock(File target); 33 | 34 | /** 35 | * Obtain a lock object that may be used to lock the target file for writing. This method must not lock that file 36 | * right immediately (see {@link Lock#lock()}). 37 | * 38 | * @param target the file to lock, never {@code null}. 39 | * @return a lock object, never {@code null}. 40 | */ 41 | Lock writeLock(File target); 42 | 43 | // 44 | // This will become a concurrent/process safe file manager and we'll move many of the methods from the LockingFileProcessor and then 45 | // make the LockingFileProcessor a thin wrapper around our default FileManager 46 | // 47 | boolean mkdirs(File directory); 48 | 49 | void write(File target, String data) throws IOException; 50 | 51 | void write(File target, InputStream source) throws IOException; 52 | 53 | void move(File source, File target) throws IOException; 54 | 55 | void copy(File source, File target) throws IOException; 56 | 57 | long copy(File source, File target, ProgressListener listener) throws IOException; 58 | 59 | /** 60 | * A listener object that is notified for every progress made while copying files. 61 | * 62 | * @see FileProcessor#copy(File, File, ProgressListener) 63 | */ 64 | public interface ProgressListener { 65 | void progressed(ByteBuffer buffer) throws IOException; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/io/takari/filemanager/Lock.java: -------------------------------------------------------------------------------- 1 | package io.takari.filemanager; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.io.RandomAccessFile; 6 | import java.nio.channels.FileLock; 7 | 8 | /** 9 | * This lock object adds the ability to directly access the contents of the locked file. 10 | * 11 | * @author Benjamin Hanzelmann 12 | */ 13 | public interface Lock { 14 | 15 | /** 16 | * Gets the random access file used to read/write the contents of the locked file. 17 | * 18 | * @return The random access file used to read/write or {@code null} if the lock isn't acquired. 19 | * @throws IOException 20 | */ 21 | RandomAccessFile getRandomAccessFile() throws IOException; 22 | 23 | /** 24 | * Tells whether the lock is shared or exclusive. 25 | * 26 | * @return {@code true} if the lock is shared, {@code false} if the lock is exclusive. 27 | */ 28 | boolean isShared(); 29 | 30 | /** 31 | * Lock the file this Lock was obtained for. 32 | *

33 | * Multiple {@link #lock()} invocations on the same or other lock objects using the same (canonical) file as 34 | * target are possible and non-blocking from the same caller thread. 35 | * 36 | * @throws IOException if an error occurs while locking the file. 37 | */ 38 | void lock() throws IOException; 39 | 40 | /** 41 | * Unlock the file this Lock was obtained for. 42 | * 43 | * @throws IOException if an error occurs while locking the file. 44 | */ 45 | void unlock() throws IOException; 46 | 47 | /** 48 | * Get the file this Lock was obtained for. 49 | * 50 | * @return The file this lock was obtained for, never {@code null}. 51 | */ 52 | File getFile(); 53 | 54 | // I added this to remove having to reference the concrete implementation in the test case 55 | FileLock getLock(); 56 | } -------------------------------------------------------------------------------- /src/main/java/io/takari/filemanager/internal/DefaultFileManager.java: -------------------------------------------------------------------------------- 1 | package io.takari.filemanager.internal; 2 | 3 | /******************************************************************************* 4 | * Copyright (c) 2010-2013 Sonatype, Inc. 5 | * All rights reserved. This program and the accompanying materials 6 | * are made available under the terms of the Eclipse Public License v1.0 7 | * which accompanies this distribution, and is available at 8 | * http://www.eclipse.org/legal/epl-v10.html 9 | *******************************************************************************/ 10 | 11 | import io.takari.filemanager.FileManager; 12 | import io.takari.filemanager.Lock; 13 | 14 | import java.io.Closeable; 15 | import java.io.File; 16 | import java.io.IOException; 17 | import java.io.InputStream; 18 | import java.io.RandomAccessFile; 19 | import java.nio.ByteBuffer; 20 | import java.nio.channels.FileLock; 21 | import java.nio.channels.FileLockInterruptionException; 22 | import java.util.HashMap; 23 | import java.util.Locale; 24 | import java.util.Map; 25 | import java.util.concurrent.ConcurrentHashMap; 26 | import java.util.concurrent.ConcurrentMap; 27 | import java.util.concurrent.atomic.AtomicInteger; 28 | 29 | import javax.inject.Named; 30 | import javax.inject.Singleton; 31 | 32 | import org.slf4j.Logger; 33 | import org.slf4j.LoggerFactory; 34 | 35 | /** 36 | * Offers advisory file locking independently of the platform. With regard to concurrent readers that don't use any file 37 | * locking (i.e. 3rd party code accessing files), mandatory locking (as seen on Windows) must be avoided as this would 38 | * immediately kill the unaware readers. To emulate advisory locking, this implementation uses a dedicated lock file 39 | * (*.aetherlock) next to the actual file. The inter-process file locking is performed on this lock file, thereby 40 | * keeping the data file free from locking. 41 | * 42 | * @author Benjamin Bentmann 43 | */ 44 | @Named 45 | @Singleton 46 | public class DefaultFileManager implements FileManager { 47 | 48 | private static Boolean IS_SET_LAST_MODIFIED_SAFE; 49 | 50 | private Logger logger = LoggerFactory.getLogger(DefaultFileManager.class); 51 | private static final ConcurrentMap lockFiles = new ConcurrentHashMap(64); 52 | 53 | public Lock readLock(File target) { 54 | return new IndirectFileLock(normalize(target), false); 55 | } 56 | 57 | public Lock writeLock(File target) { 58 | return new IndirectFileLock(normalize(target), true); 59 | } 60 | 61 | private File normalize(File file) { 62 | try { 63 | return file.getCanonicalFile(); 64 | } catch (IOException e) { 65 | logger.warn("Failed to normalize pathname for lock on " + file + ": " + e); 66 | return file.getAbsoluteFile(); 67 | } 68 | } 69 | 70 | /** 71 | * Null-safe variant of {@link File#mkdirs()}. 72 | * 73 | * @param directory The directory to create, may be {@code null}. 74 | * @return {@code true} if and only if the directory was created, along with all necessary parent 75 | * directories; {@code false} otherwise 76 | */ 77 | public boolean mkdirs(File directory) { 78 | if (directory == null) { 79 | return false; 80 | } 81 | 82 | return directory.mkdirs(); 83 | } 84 | 85 | private RandomAccessFile open(File file, String mode) throws IOException { 86 | boolean interrupted = false; 87 | 88 | try { 89 | mkdirs(file.getParentFile()); 90 | 91 | return new RandomAccessFile(file, mode); 92 | } catch (IOException e) { 93 | /* 94 | * NOTE: I've seen failures (on Windows) when opening the file which I can't really explain ("access denied", "locked"). Assuming those are bad interactions with OS-level processes (e.g. 95 | * indexing, anti-virus), let's just retry before giving up due to a potentially spurious problem. 96 | */ 97 | for (int i = 3; i >= 0; i--) { 98 | try { 99 | Thread.sleep(10); 100 | } catch (InterruptedException e1) { 101 | interrupted = true; 102 | } 103 | try { 104 | return new RandomAccessFile(file, mode); 105 | } catch (IOException ie) { 106 | // ignored, we eventually rethrow the original error 107 | } 108 | } 109 | 110 | throw e; 111 | } finally { 112 | if (interrupted) { 113 | Thread.currentThread().interrupt(); 114 | } 115 | } 116 | } 117 | 118 | private void close(Closeable closeable) { 119 | if (closeable != null) { 120 | try { 121 | closeable.close(); 122 | } catch (IOException e) { 123 | if (logger != null) { 124 | logger.warn("Failed to close file: " + e); 125 | } 126 | } 127 | } 128 | } 129 | 130 | class IndirectFileLock implements Lock { 131 | 132 | private final File file; 133 | private final boolean write; 134 | private final Throwable stackTrace; 135 | private RandomAccessFile raFile; 136 | private LockFile lockFile; 137 | private int nesting; 138 | 139 | public IndirectFileLock(File file, boolean write) { 140 | this.file = file; 141 | this.write = write; 142 | this.stackTrace = new IllegalStateException(); 143 | } 144 | 145 | public synchronized void lock() throws IOException { 146 | if (lockFile == null) { 147 | open(); 148 | nesting = 1; 149 | } else { 150 | nesting++; 151 | } 152 | } 153 | 154 | public synchronized void unlock() throws IOException { 155 | nesting--; 156 | if (nesting <= 0) { 157 | close(); 158 | } 159 | } 160 | 161 | public RandomAccessFile getRandomAccessFile() throws IOException { 162 | if (raFile == null && lockFile != null && lockFile.getFileLock().isValid()) { 163 | raFile = DefaultFileManager.this.open(file, write ? "rw" : "r"); 164 | } 165 | return raFile; 166 | } 167 | 168 | public boolean isShared() { 169 | if (lockFile == null) { 170 | throw new IllegalStateException("lock not acquired"); 171 | } 172 | return lockFile.isShared(); 173 | } 174 | 175 | public FileLock getLock() { 176 | if (lockFile == null) { 177 | return null; 178 | } 179 | return lockFile.getFileLock(); 180 | } 181 | 182 | public File getFile() { 183 | return file; 184 | } 185 | 186 | @Override 187 | protected void finalize() throws Throwable { 188 | try { 189 | if (lockFile != null) { 190 | logger.warn("Lock on file " + file + " has not been properly released", stackTrace); 191 | } 192 | close(); 193 | } finally { 194 | super.finalize(); 195 | } 196 | } 197 | 198 | private void open() throws IOException { 199 | lockFile = lock(file, write); 200 | } 201 | 202 | private void close() throws IOException { 203 | try { 204 | if (raFile != null) { 205 | try { 206 | raFile.close(); 207 | } finally { 208 | raFile = null; 209 | } 210 | } 211 | } finally { 212 | if (lockFile != null) { 213 | try { 214 | unlock(lockFile); 215 | } catch (IOException e) { 216 | logger.warn("Failed to release lock for " + file + ": " + e); 217 | } finally { 218 | lockFile = null; 219 | } 220 | } 221 | } 222 | } 223 | 224 | private LockFile lock(File file, boolean write) throws IOException { 225 | boolean interrupted = false; 226 | 227 | try { 228 | while (true) { 229 | LockFile lockFile = lockFiles.get(file); 230 | 231 | if (lockFile == null) { 232 | lockFile = new LockFile(file); 233 | 234 | LockFile existing = lockFiles.putIfAbsent(file, lockFile); 235 | if (existing != null) { 236 | lockFile = existing; 237 | } 238 | } 239 | 240 | synchronized (lockFile) { 241 | if (lockFile.isInvalid()) { 242 | continue; 243 | } else if (lockFile.lock(write)) { 244 | return lockFile; 245 | } 246 | 247 | try { 248 | lockFile.wait(); 249 | } catch (InterruptedException e) { 250 | interrupted = true; 251 | } 252 | } 253 | } 254 | } finally { 255 | /* 256 | * NOTE: We want to ignore the interrupt but other code might want/need to react to it, so restore the interrupt flag. 257 | */ 258 | if (interrupted) { 259 | Thread.currentThread().interrupt(); 260 | } 261 | } 262 | } 263 | 264 | private void unlock(LockFile lockFile) throws IOException { 265 | synchronized (lockFile) { 266 | try { 267 | lockFile.unlock(); 268 | } finally { 269 | if (lockFile.isInvalid()) { 270 | lockFiles.remove(lockFile.getDataFile(), lockFile); 271 | lockFile.notifyAll(); 272 | } 273 | } 274 | } 275 | } 276 | } 277 | 278 | // LockFile 279 | 280 | /** 281 | * Manages an {@code *.aetherlock} file. Note: This class is not thread-safe and requires external 282 | * synchronization. 283 | */ 284 | class LockFile { 285 | 286 | private final File dataFile; 287 | private final File lockFile; 288 | private FileLock fileLock; 289 | private RandomAccessFile raFile; 290 | private int refCount; 291 | private Thread owner; 292 | private final Map clients = new HashMap(); 293 | 294 | public LockFile(File dataFile) { 295 | this.dataFile = dataFile; 296 | if (dataFile.isDirectory()) { 297 | lockFile = new File(dataFile, ".aetherlock"); 298 | } else { 299 | lockFile = new File(dataFile.getPath() + ".aetherlock"); 300 | } 301 | } 302 | 303 | public File getDataFile() { 304 | return dataFile; 305 | } 306 | 307 | public boolean lock(boolean write) throws IOException { 308 | if (isInvalid()) { 309 | throw new IllegalStateException("lock for " + dataFile + " has been invalidated"); 310 | } 311 | 312 | if (isClosed()) { 313 | open(write); 314 | 315 | return true; 316 | } else if (isReentrant(write)) { 317 | incRefCount(); 318 | 319 | return true; 320 | } else if (isAlreadyHoldByCurrentThread()) { 321 | throw new IllegalStateException("Cannot acquire " + (write ? "write" : "read") + " lock on " + dataFile + " for thread " + Thread.currentThread() + " which already holds incompatible lock"); 322 | } 323 | 324 | return false; 325 | } 326 | 327 | public void unlock() throws IOException { 328 | if (decRefCount() <= 0) { 329 | close(); 330 | } 331 | } 332 | 333 | FileLock getFileLock() { 334 | return fileLock; 335 | } 336 | 337 | public boolean isInvalid() { 338 | return refCount < 0; 339 | } 340 | 341 | public boolean isShared() { 342 | if (fileLock == null) { 343 | throw new IllegalStateException("lock not acquired"); 344 | } 345 | return fileLock.isShared(); 346 | } 347 | 348 | private boolean isClosed() { 349 | return fileLock == null; 350 | } 351 | 352 | private boolean isReentrant(boolean write) { 353 | if (isShared()) { 354 | return !write; 355 | } else { 356 | return Thread.currentThread() == owner; 357 | } 358 | } 359 | 360 | private boolean isAlreadyHoldByCurrentThread() { 361 | return clients.get(Thread.currentThread()) != null; 362 | } 363 | 364 | private void open(boolean write) throws IOException { 365 | refCount = 1; 366 | 367 | owner = write ? Thread.currentThread() : null; 368 | 369 | clients.put(Thread.currentThread(), new AtomicInteger(1)); 370 | 371 | RandomAccessFile raf = null; 372 | FileLock lock = null; 373 | boolean interrupted = false; 374 | 375 | try { 376 | while (true) { 377 | raf = DefaultFileManager.this.open(lockFile, "rw"); 378 | 379 | try { 380 | lock = raf.getChannel().lock(0, 1, !write); 381 | 382 | if (lock == null) { 383 | /* 384 | * Probably related to http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6979009, lock() erroneously returns null when the thread got interrupted and the channel silently closed. 385 | */ 386 | throw new FileLockInterruptionException(); 387 | } 388 | 389 | break; 390 | } catch (FileLockInterruptionException e) { 391 | /* 392 | * NOTE: We want to lock that file and this isn't negotiable, so whatever felt like interrupting our thread, try again later, we have work to get done. And since the interrupt closed the 393 | * channel, we need to start with a fresh file handle. 394 | */ 395 | 396 | interrupted |= Thread.interrupted(); 397 | 398 | DefaultFileManager.this.close(raf); 399 | } catch (IOException e) { 400 | DefaultFileManager.this.close(raf); 401 | 402 | // EVIL: parse message of IOException to find out if it's a (probably erroneous) 'deadlock 403 | // detection' (linux kernel does not account for different threads holding the locks for the 404 | // same process) 405 | if (isPseudoDeadlock(e)) { 406 | logger.debug("OS detected pseudo deadlock for " + lockFile + ", retrying locking"); 407 | try { 408 | Thread.sleep(100); 409 | } catch (InterruptedException e1) { 410 | interrupted = true; 411 | } 412 | } else { 413 | delete(); 414 | throw e; 415 | } 416 | } 417 | } 418 | } finally { 419 | /* 420 | * NOTE: We want to ignore the interrupt but other code might want/need to react to it, so restore the interrupt flag. 421 | */ 422 | if (interrupted) { 423 | Thread.currentThread().interrupt(); 424 | } 425 | } 426 | 427 | raFile = raf; 428 | fileLock = lock; 429 | } 430 | 431 | private boolean isPseudoDeadlock(IOException e) { 432 | String msg = e.getMessage(); 433 | return msg != null && msg.toLowerCase(Locale.ENGLISH).contains("deadlock"); 434 | } 435 | 436 | private void close() throws IOException { 437 | refCount = -1; 438 | 439 | if (fileLock != null) { 440 | try { 441 | if (fileLock.isValid()) { 442 | fileLock.release(); 443 | } 444 | } catch (IOException e) { 445 | logger.warn("Failed to release lock on " + lockFile + ": " + e); 446 | } finally { 447 | fileLock = null; 448 | } 449 | } 450 | 451 | if (raFile != null) { 452 | try { 453 | raFile.close(); 454 | } finally { 455 | raFile = null; 456 | delete(); 457 | } 458 | } 459 | } 460 | 461 | private void delete() { 462 | if (lockFile != null) { 463 | if (!lockFile.delete() && lockFile.exists()) { 464 | // NOTE: This happens naturally when some other thread locked it in the meantime 465 | lockFile.deleteOnExit(); 466 | } 467 | } 468 | } 469 | 470 | private int incRefCount() { 471 | AtomicInteger clientRefCount = clients.get(Thread.currentThread()); 472 | if (clientRefCount == null) { 473 | clients.put(Thread.currentThread(), new AtomicInteger(1)); 474 | } else { 475 | clientRefCount.incrementAndGet(); 476 | } 477 | 478 | return ++refCount; 479 | } 480 | 481 | private int decRefCount() { 482 | AtomicInteger clientRefCount = clients.get(Thread.currentThread()); 483 | if (clientRefCount != null && clientRefCount.decrementAndGet() <= 0) { 484 | clients.remove(Thread.currentThread()); 485 | } 486 | 487 | return --refCount; 488 | } 489 | 490 | @Override 491 | protected void finalize() throws Throwable { 492 | try { 493 | close(); 494 | } finally { 495 | super.finalize(); 496 | } 497 | } 498 | } 499 | 500 | // FileManager 501 | 502 | private void unlock(Lock lock) { 503 | if (lock != null) { 504 | try { 505 | lock.unlock(); 506 | } catch (IOException e) { 507 | logger.warn("Failed to unlock file " + lock.getFile() + ": " + e); 508 | } 509 | } 510 | } 511 | 512 | public void copy(File source, File target) throws IOException { 513 | copy(source, target, null); 514 | } 515 | 516 | /** 517 | * Copy src- to target-file. Creates the necessary directories for the target file. In case of an error, the created 518 | * directories will be left on the file system. 519 | *

520 | * This method performs R/W-locking on the given files to provide concurrent access to files without data 521 | * corruption, and will honor {@link FileLock}s from an external process. 522 | * 523 | * @param source the file to copy from, must not be {@code null}. 524 | * @param target the file to copy to, must not be {@code null}. 525 | * @param listener the listener to notify about the copy progress, may be {@code null}. 526 | * @return the number of copied bytes. 527 | * @throws IOException if an I/O error occurs. 528 | */ 529 | public long copy(File source, File target, ProgressListener listener) throws IOException { 530 | Lock sourceLock = readLock(source); 531 | Lock targetLock = writeLock(target); 532 | 533 | try { 534 | mkdirs(target.getParentFile()); 535 | 536 | sourceLock.lock(); 537 | targetLock.lock(); 538 | 539 | return copy(sourceLock.getRandomAccessFile(), targetLock.getRandomAccessFile(), listener); 540 | } finally { 541 | target.setLastModified(source.lastModified()); 542 | unlock(sourceLock); 543 | unlock(targetLock); 544 | } 545 | } 546 | 547 | private long copy(RandomAccessFile rafIn, RandomAccessFile rafOut, ProgressListener listener) throws IOException { 548 | ByteBuffer buffer = ByteBuffer.allocate(1024 * 32); 549 | byte[] array = buffer.array(); 550 | long total = 0; 551 | for (;;) { 552 | int bytes = rafIn.read(array); 553 | if (bytes < 0) { 554 | rafOut.setLength(rafOut.getFilePointer()); 555 | break; 556 | } 557 | rafOut.write(array, 0, bytes); 558 | total += bytes; 559 | if (listener != null && bytes > 0) { 560 | try { 561 | buffer.rewind(); 562 | buffer.limit(bytes); 563 | listener.progressed(buffer); 564 | } catch (Exception e) { 565 | logger.debug("Failed to invoke copy progress listener", e); 566 | } 567 | } 568 | } 569 | return total; 570 | } 571 | 572 | public void write(File file, InputStream source) throws IOException { 573 | 574 | Lock lock = writeLock(file); 575 | 576 | try { 577 | mkdirs(file.getParentFile()); 578 | 579 | lock.lock(); 580 | 581 | RandomAccessFile raf = lock.getRandomAccessFile(); 582 | 583 | raf.seek(0); 584 | if (source != null) { 585 | byte[] buffer = new byte[1024]; 586 | int len; 587 | while ((len = source.read(buffer)) != -1) { 588 | raf.write(buffer, 0, len); 589 | } 590 | } 591 | 592 | raf.setLength(raf.getFilePointer()); 593 | } finally { 594 | unlock(lock); 595 | } 596 | } 597 | 598 | /** 599 | * Write the given data to a file. UTF-8 is assumed as encoding for the data. 600 | * 601 | * @param file The file to write to, must not be {@code null}. This file will be truncated. 602 | * @param data The data to write, may be {@code null}. 603 | * @throws IOException if an I/O error occurs. 604 | */ 605 | public void write(File file, String data) throws IOException { 606 | Lock lock = writeLock(file); 607 | 608 | try { 609 | mkdirs(file.getParentFile()); 610 | 611 | lock.lock(); 612 | 613 | RandomAccessFile raf = lock.getRandomAccessFile(); 614 | 615 | raf.seek(0); 616 | if (data != null) { 617 | raf.write(data.getBytes("UTF-8")); 618 | } 619 | 620 | raf.setLength(raf.getFilePointer()); 621 | } finally { 622 | unlock(lock); 623 | } 624 | } 625 | 626 | public void move(File source, File target) throws IOException { 627 | /* 628 | * NOTE: For graceful collaboration with concurrent readers don't attempt to delete the target file, if it already exists, it's safer to just overwrite it, especially when the contents doesn't 629 | * actually change. 630 | */ 631 | 632 | /* 633 | * NOTE: We're about to remove/delete the source file so be sure to acquire an exclusive lock for the source. 634 | */ 635 | 636 | Lock sourceLock = writeLock(source); 637 | Lock targetLock = writeLock(target); 638 | 639 | try { 640 | mkdirs(target.getParentFile()); 641 | 642 | sourceLock.lock(); 643 | targetLock.lock(); 644 | 645 | if (!source.renameTo(target)) { 646 | copy(sourceLock.getRandomAccessFile(), targetLock.getRandomAccessFile(), null); 647 | 648 | /* 649 | * NOTE: On Windows and before JRE 1.7, File.setLastModified() opens the file without any sharing enabled (cf. evaluation of Sun bug 6357599). This means while setLastModified() is executing, 650 | * no other thread/process can open the file "because it is being used by another process". The read accesses to files can't always be guarded by locks, take for instance class loaders reading 651 | * JARs, so we must avoid calling setLastModified() completely on the affected platforms to enable safe concurrent IO. The setLastModified() call below while the file is still open is 652 | * generally ineffective as the OS will update the timestamp after closing the file (at least Windows does so). But its failure allows us to detect the problematic platforms. The destination 653 | * file not having the same timestamp as the source file isn't overly beauty but shouldn't actually matter in real life either. 654 | */ 655 | if (IS_SET_LAST_MODIFIED_SAFE == null) { 656 | IS_SET_LAST_MODIFIED_SAFE = Boolean.valueOf(target.setLastModified(source.lastModified())); 657 | logger.debug("Updates of file modification timestamp are safe: " + IS_SET_LAST_MODIFIED_SAFE); 658 | } 659 | 660 | close(targetLock.getRandomAccessFile()); 661 | 662 | if (IS_SET_LAST_MODIFIED_SAFE.booleanValue()) { 663 | target.setLastModified(source.lastModified()); 664 | } 665 | 666 | // NOTE: Close the file handle to enable its deletion but don't release the lock yet. 667 | close(sourceLock.getRandomAccessFile()); 668 | 669 | source.delete(); 670 | } 671 | } finally { 672 | unlock(sourceLock); 673 | unlock(targetLock); 674 | } 675 | } 676 | } 677 | -------------------------------------------------------------------------------- /src/main/java/org/eclipse/aether/internal/impl/DefaultUpdateCheckManager.java: -------------------------------------------------------------------------------- 1 | package org.eclipse.aether.internal.impl; 2 | 3 | import io.takari.aether.localrepo.TakariUpdateCheckManager; 4 | 5 | import javax.inject.Named; 6 | import javax.inject.Singleton; 7 | 8 | @Named 9 | @Singleton 10 | public class DefaultUpdateCheckManager extends TakariUpdateCheckManager { 11 | } 12 | -------------------------------------------------------------------------------- /src/test/java/io/takari/aether/localrepo/BaseLocalRepositoryManagerTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2010, 2013 Sonatype, Inc. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the Eclipse Public License v1.0 5 | * which accompanies this distribution, and is available at 6 | * http://www.eclipse.org/legal/epl-v10.html 7 | * 8 | * Contributors: 9 | * Sonatype, Inc. - initial API and implementation 10 | *******************************************************************************/ 11 | package io.takari.aether.localrepo; 12 | 13 | import static org.junit.Assert.*; 14 | 15 | import java.io.File; 16 | import java.io.IOException; 17 | 18 | import org.eclipse.aether.RepositorySystemSession; 19 | import org.eclipse.aether.artifact.Artifact; 20 | import org.eclipse.aether.artifact.DefaultArtifact; 21 | import org.eclipse.aether.internal.test.util.TestUtils; 22 | import org.eclipse.aether.repository.LocalArtifactRequest; 23 | import org.eclipse.aether.repository.LocalArtifactResult; 24 | import org.eclipse.aether.repository.RemoteRepository; 25 | import org.junit.After; 26 | import org.junit.Before; 27 | import org.junit.Test; 28 | 29 | import com.google.common.collect.Lists; 30 | 31 | public class BaseLocalRepositoryManagerTest { 32 | 33 | private File basedir; 34 | 35 | private TakariLocalRepositoryManager manager; 36 | 37 | private RepositorySystemSession session; 38 | 39 | @Before 40 | public void setup() throws IOException { 41 | basedir = TestFileUtils.createTempDir("simple-repo"); 42 | session = TestUtils.newSession(); 43 | manager = new TakariLocalRepositoryManager(basedir, session, Lists.newArrayList()); 44 | } 45 | 46 | @After 47 | public void tearDown() throws Exception { 48 | TestFileUtils.delete(basedir); 49 | manager = null; 50 | session = null; 51 | } 52 | 53 | @Test 54 | public void testGetPathForLocalArtifact() throws Exception { 55 | Artifact artifact = new DefaultArtifact("g.i.d:a.i.d:1.0-SNAPSHOT"); 56 | assertEquals("1.0-SNAPSHOT", artifact.getBaseVersion()); 57 | assertEquals("g/i/d/a.i.d/1.0-SNAPSHOT/a.i.d-1.0-SNAPSHOT.jar", manager.getPathForLocalArtifact(artifact)); 58 | 59 | artifact = new DefaultArtifact("g.i.d:a.i.d:1.0-20110329.221805-4"); 60 | assertEquals("1.0-SNAPSHOT", artifact.getBaseVersion()); 61 | assertEquals("g/i/d/a.i.d/1.0-SNAPSHOT/a.i.d-1.0-SNAPSHOT.jar", manager.getPathForLocalArtifact(artifact)); 62 | 63 | artifact = new DefaultArtifact("g.i.d", "a.i.d", "", "", "1.0-SNAPSHOT"); 64 | assertEquals("g/i/d/a.i.d/1.0-SNAPSHOT/a.i.d-1.0-SNAPSHOT", manager.getPathForLocalArtifact(artifact)); 65 | } 66 | 67 | @Test 68 | public void testGetPathForRemoteArtifact() throws Exception { 69 | RemoteRepository remoteRepo = new RemoteRepository.Builder("repo", "default", "ram:/void").build(); 70 | 71 | Artifact artifact = new DefaultArtifact("g.i.d:a.i.d:1.0-SNAPSHOT"); 72 | assertEquals("1.0-SNAPSHOT", artifact.getBaseVersion()); 73 | assertEquals("g/i/d/a.i.d/1.0-SNAPSHOT/a.i.d-1.0-SNAPSHOT.jar", manager.getPathForRemoteArtifact(artifact, remoteRepo, "")); 74 | 75 | artifact = new DefaultArtifact("g.i.d:a.i.d:1.0-20110329.221805-4"); 76 | assertEquals("1.0-SNAPSHOT", artifact.getBaseVersion()); 77 | assertEquals("g/i/d/a.i.d/1.0-SNAPSHOT/a.i.d-1.0-20110329.221805-4.jar", manager.getPathForRemoteArtifact(artifact, remoteRepo, "")); 78 | } 79 | 80 | @Test 81 | public void testFindArtifactUsesTimestampedVersion() throws Exception { 82 | Artifact artifact = new DefaultArtifact("g.i.d:a.i.d:1.0-SNAPSHOT"); 83 | File file = new File(basedir, manager.getPathForLocalArtifact(artifact)); 84 | TestFileUtils.write("test", file); 85 | 86 | artifact = artifact.setVersion("1.0-20110329.221805-4"); 87 | LocalArtifactRequest request = new LocalArtifactRequest(); 88 | request.setArtifact(artifact); 89 | LocalArtifactResult result = manager.find(session, request); 90 | assertNull(result.toString(), result.getFile()); 91 | assertFalse(result.toString(), result.isAvailable()); 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/test/java/io/takari/aether/localrepo/SimpleResolutionErrorPolicy.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2012 Sonatype, Inc. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the Eclipse Public License v1.0 5 | * which accompanies this distribution, and is available at 6 | * http://www.eclipse.org/legal/epl-v10.html 7 | * 8 | * Contributors: 9 | * Sonatype, Inc. - initial API and implementation 10 | *******************************************************************************/ 11 | package io.takari.aether.localrepo; 12 | 13 | import org.eclipse.aether.RepositorySystemSession; 14 | import org.eclipse.aether.artifact.Artifact; 15 | import org.eclipse.aether.metadata.Metadata; 16 | import org.eclipse.aether.resolution.ResolutionErrorPolicy; 17 | import org.eclipse.aether.resolution.ResolutionErrorPolicyRequest; 18 | 19 | /** 20 | * A resolution error policy that allows to control caching for artifacts and metadata at a global level. 21 | */ 22 | public final class SimpleResolutionErrorPolicy 23 | implements ResolutionErrorPolicy 24 | { 25 | 26 | private final int artifactPolicy; 27 | 28 | private final int metadataPolicy; 29 | 30 | /** 31 | * Creates a new error policy with the specified behavior for both artifacts and metadata. 32 | * 33 | * @param cacheNotFound {@code true} to enable caching of missing items, {@code false} to disable it. 34 | * @param cacheTransferErrors {@code true} to enable chaching of transfer errors, {@code false} to disable it. 35 | */ 36 | public SimpleResolutionErrorPolicy( boolean cacheNotFound, boolean cacheTransferErrors ) 37 | { 38 | this( ( cacheNotFound ? CACHE_NOT_FOUND : 0 ) | ( cacheTransferErrors ? CACHE_TRANSFER_ERROR : 0 ) ); 39 | } 40 | 41 | /** 42 | * Creates a new error policy with the specified bit mask for both artifacts and metadata. 43 | * 44 | * @param policy The bit mask describing the policy for artifacts and metadata. 45 | */ 46 | public SimpleResolutionErrorPolicy( int policy ) 47 | { 48 | this( policy, policy ); 49 | } 50 | 51 | /** 52 | * Creates a new error policy with the specified bit masks for artifacts and metadata. 53 | * 54 | * @param artifactPolicy The bit mask describing the policy for artifacts. 55 | * @param metadataPolicy The bit mask describing the policy for metadata. 56 | */ 57 | public SimpleResolutionErrorPolicy( int artifactPolicy, int metadataPolicy ) 58 | { 59 | this.artifactPolicy = artifactPolicy; 60 | this.metadataPolicy = metadataPolicy; 61 | } 62 | 63 | public int getArtifactPolicy( RepositorySystemSession session, ResolutionErrorPolicyRequest request ) 64 | { 65 | return artifactPolicy; 66 | } 67 | 68 | public int getMetadataPolicy( RepositorySystemSession session, ResolutionErrorPolicyRequest request ) 69 | { 70 | return metadataPolicy; 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/test/java/io/takari/aether/localrepo/TakariLocalRepositoryManagerTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2010, 2013 Sonatype, Inc. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the Eclipse Public License v1.0 5 | * which accompanies this distribution, and is available at 6 | * http://www.eclipse.org/legal/epl-v10.html 7 | * 8 | * Contributors: 9 | * Sonatype, Inc. - initial API and implementation 10 | *******************************************************************************/ 11 | package io.takari.aether.localrepo; 12 | 13 | import static org.junit.Assert.assertEquals; 14 | import static org.junit.Assert.assertFalse; 15 | import static org.junit.Assert.assertNotNull; 16 | import static org.junit.Assert.assertNull; 17 | import static org.junit.Assert.assertTrue; 18 | 19 | import java.io.File; 20 | import java.io.IOException; 21 | import java.net.URI; 22 | import java.util.Arrays; 23 | import java.util.Collection; 24 | import java.util.Collections; 25 | 26 | import org.eclipse.aether.RepositorySystemSession; 27 | import org.eclipse.aether.artifact.Artifact; 28 | import org.eclipse.aether.artifact.DefaultArtifact; 29 | import org.eclipse.aether.internal.test.util.TestUtils; 30 | import org.eclipse.aether.metadata.DefaultMetadata; 31 | import org.eclipse.aether.metadata.Metadata; 32 | import org.eclipse.aether.metadata.Metadata.Nature; 33 | import org.eclipse.aether.repository.LocalArtifactRegistration; 34 | import org.eclipse.aether.repository.LocalArtifactRequest; 35 | import org.eclipse.aether.repository.LocalArtifactResult; 36 | import org.eclipse.aether.repository.LocalMetadataRequest; 37 | import org.eclipse.aether.repository.LocalMetadataResult; 38 | import org.eclipse.aether.repository.RemoteRepository; 39 | import org.junit.After; 40 | import org.junit.Before; 41 | import org.junit.Test; 42 | 43 | import com.google.common.collect.Lists; 44 | 45 | public class TakariLocalRepositoryManagerTest { 46 | 47 | private Artifact artifact; 48 | private Artifact snapshot; 49 | private File basedir; 50 | private TakariLocalRepositoryManager manager; 51 | private File artifactFile; 52 | private RemoteRepository repository; 53 | private String testContext = "project/compile"; 54 | private RepositorySystemSession session; 55 | private Metadata metadata; 56 | private Metadata noVerMetadata; 57 | 58 | @Before 59 | public void setup() throws Exception { 60 | String url = TestFileUtils.createTempDir("enhanced-remote-repo").toURI().toURL().toString(); 61 | repository = new RemoteRepository.Builder("enhanced-remote-repo", "default", url).setRepositoryManager(true).build(); 62 | artifact = new DefaultArtifact("gid", "aid", "", "jar", "1-test", Collections. emptyMap(), TestFileUtils.createTempFile("artifact")); 63 | snapshot = new DefaultArtifact("gid", "aid", "", "jar", "1.0-20120710.231549-9", Collections. emptyMap(), TestFileUtils.createTempFile("artifact")); 64 | metadata = new DefaultMetadata("gid", "aid", "1-test", "maven-metadata.xml", Nature.RELEASE, TestFileUtils.createTempFile("metadata")); 65 | noVerMetadata = new DefaultMetadata("gid", "aid", null, "maven-metadata.xml", Nature.RELEASE, TestFileUtils.createTempFile("metadata")); 66 | basedir = TestFileUtils.createTempDir("enhanced-repo"); 67 | session = TestUtils.newSession(); 68 | manager = new TakariLocalRepositoryManager(basedir, session, Lists. newArrayList()); 69 | artifactFile = new File(basedir, manager.getPathForLocalArtifact(artifact)); 70 | } 71 | 72 | @After 73 | public void tearDown() throws Exception { 74 | TestFileUtils.delete(basedir); 75 | TestFileUtils.delete(new File(new URI(repository.getUrl()))); 76 | 77 | session = null; 78 | manager = null; 79 | repository = null; 80 | artifact = null; 81 | } 82 | 83 | private long addLocalArtifact(Artifact artifact) throws IOException { 84 | manager.add(session, new LocalArtifactRegistration(artifact)); 85 | String path = manager.getPathForLocalArtifact(artifact); 86 | 87 | return copy(artifact, path); 88 | } 89 | 90 | private long addRemoteArtifact(Artifact artifact) throws IOException { 91 | Collection contexts = Arrays.asList(testContext); 92 | manager.add(session, new LocalArtifactRegistration(artifact, repository, contexts)); 93 | String path = manager.getPathForRemoteArtifact(artifact, repository, testContext); 94 | return copy(artifact, path); 95 | } 96 | 97 | private long copy(Metadata metadata, String path) throws IOException { 98 | if (metadata.getFile() == null) { 99 | return -1; 100 | } 101 | return TestFileUtils.copy(metadata.getFile(), new File(basedir, path)); 102 | } 103 | 104 | private long copy(Artifact artifact, String path) throws IOException { 105 | if (artifact.getFile() == null) { 106 | return -1; 107 | } 108 | File artifactFile = new File(basedir, path); 109 | return TestFileUtils.copy(artifact.getFile(), artifactFile); 110 | } 111 | 112 | @Test 113 | public void testGetPathForLocalArtifact() { 114 | Artifact artifact = new DefaultArtifact("g.i.d:a.i.d:1.0-SNAPSHOT"); 115 | assertEquals("1.0-SNAPSHOT", artifact.getBaseVersion()); 116 | assertEquals("g/i/d/a.i.d/1.0-SNAPSHOT/a.i.d-1.0-SNAPSHOT.jar", manager.getPathForLocalArtifact(artifact)); 117 | 118 | artifact = new DefaultArtifact("g.i.d:a.i.d:1.0-20110329.221805-4"); 119 | assertEquals("1.0-SNAPSHOT", artifact.getBaseVersion()); 120 | assertEquals("g/i/d/a.i.d/1.0-SNAPSHOT/a.i.d-1.0-SNAPSHOT.jar", manager.getPathForLocalArtifact(artifact)); 121 | } 122 | 123 | @Test 124 | public void testGetPathForRemoteArtifact() { 125 | RemoteRepository remoteRepo = new RemoteRepository.Builder("repo", "default", "ram:/void").build(); 126 | 127 | Artifact artifact = new DefaultArtifact("g.i.d:a.i.d:1.0-SNAPSHOT"); 128 | assertEquals("1.0-SNAPSHOT", artifact.getBaseVersion()); 129 | assertEquals("g/i/d/a.i.d/1.0-SNAPSHOT/a.i.d-1.0-SNAPSHOT.jar", manager.getPathForRemoteArtifact(artifact, remoteRepo, "")); 130 | 131 | artifact = new DefaultArtifact("g.i.d:a.i.d:1.0-20110329.221805-4"); 132 | assertEquals("1.0-SNAPSHOT", artifact.getBaseVersion()); 133 | assertEquals("g/i/d/a.i.d/1.0-SNAPSHOT/a.i.d-1.0-20110329.221805-4.jar", manager.getPathForRemoteArtifact(artifact, remoteRepo, "")); 134 | } 135 | 136 | @Test 137 | public void testFindLocalArtifact() throws Exception { 138 | addLocalArtifact(artifact); 139 | 140 | LocalArtifactRequest request = new LocalArtifactRequest(artifact, null, null); 141 | LocalArtifactResult result = manager.find(session, request); 142 | assertTrue(result.isAvailable()); 143 | assertEquals(null, result.getRepository()); 144 | 145 | snapshot = snapshot.setVersion(snapshot.getBaseVersion()); 146 | addLocalArtifact(snapshot); 147 | 148 | request = new LocalArtifactRequest(snapshot, null, null); 149 | result = manager.find(session, request); 150 | assertTrue(result.isAvailable()); 151 | assertEquals(null, result.getRepository()); 152 | } 153 | 154 | @Test 155 | public void testFindRemoteArtifact() throws Exception { 156 | addRemoteArtifact(artifact); 157 | 158 | LocalArtifactRequest request = new LocalArtifactRequest(artifact, Arrays.asList(repository), testContext); 159 | LocalArtifactResult result = manager.find(session, request); 160 | assertTrue(result.isAvailable()); 161 | assertEquals(repository, result.getRepository()); 162 | 163 | addRemoteArtifact(snapshot); 164 | 165 | request = new LocalArtifactRequest(snapshot, Arrays.asList(repository), testContext); 166 | result = manager.find(session, request); 167 | assertTrue(result.isAvailable()); 168 | assertEquals(repository, result.getRepository()); 169 | } 170 | 171 | /* 172 | 173 | So this makes sure that if the artifact is found in a different repo it fails, but we want to start providing better checks such 174 | that we will accept the artifact if it has the same SHA1. 175 | 176 | @Test 177 | public void testDoNotFindDifferentContext() throws Exception { 178 | addRemoteArtifact(artifact); 179 | 180 | LocalArtifactRequest request = new LocalArtifactRequest(artifact, Arrays.asList(repository), "different"); 181 | LocalArtifactResult result = manager.find(session, request); 182 | assertFalse(result.isAvailable()); 183 | } 184 | 185 | */ 186 | 187 | @Test 188 | public void testDoNotFindNullFile() throws Exception { 189 | artifact = artifact.setFile(null); 190 | addLocalArtifact(artifact); 191 | 192 | LocalArtifactRequest request = new LocalArtifactRequest(artifact, Arrays.asList(repository), testContext); 193 | LocalArtifactResult result = manager.find(session, request); 194 | assertFalse(result.isAvailable()); 195 | } 196 | 197 | @Test 198 | public void testDoNotFindDeletedFile() throws Exception { 199 | addLocalArtifact(artifact); 200 | assertTrue("could not delete artifact file", artifactFile.delete()); 201 | 202 | LocalArtifactRequest request = new LocalArtifactRequest(artifact, Arrays.asList(repository), testContext); 203 | LocalArtifactResult result = manager.find(session, request); 204 | assertFalse(result.isAvailable()); 205 | } 206 | 207 | @Test 208 | public void testFindUntrackedFile() throws Exception { 209 | copy(artifact, manager.getPathForLocalArtifact(artifact)); 210 | 211 | LocalArtifactRequest request = new LocalArtifactRequest(artifact, Arrays.asList(repository), testContext); 212 | LocalArtifactResult result = manager.find(session, request); 213 | assertTrue(result.isAvailable()); 214 | } 215 | 216 | private long addMetadata(Metadata metadata, RemoteRepository repo) throws IOException { 217 | String path; 218 | if (repo == null) { 219 | path = manager.getPathForLocalMetadata(metadata); 220 | } else { 221 | path = manager.getPathForRemoteMetadata(metadata, repo, testContext); 222 | } 223 | System.err.println(path); 224 | 225 | return copy(metadata, path); 226 | } 227 | 228 | @Test 229 | public void testFindLocalMetadata() throws Exception { 230 | addMetadata(metadata, null); 231 | 232 | LocalMetadataRequest request = new LocalMetadataRequest(metadata, null, testContext); 233 | LocalMetadataResult result = manager.find(session, request); 234 | 235 | assertNotNull(result.getFile()); 236 | } 237 | 238 | @Test 239 | public void testFindLocalMetadataNoVersion() throws Exception { 240 | addMetadata(noVerMetadata, null); 241 | 242 | LocalMetadataRequest request = new LocalMetadataRequest(noVerMetadata, null, testContext); 243 | LocalMetadataResult result = manager.find(session, request); 244 | 245 | assertNotNull(result.getFile()); 246 | } 247 | 248 | @Test 249 | public void testDoNotFindRemoteMetadataDifferentContext() throws Exception { 250 | addMetadata(noVerMetadata, repository); 251 | addMetadata(metadata, repository); 252 | 253 | LocalMetadataRequest request = new LocalMetadataRequest(noVerMetadata, repository, "different"); 254 | LocalMetadataResult result = manager.find(session, request); 255 | assertNull(result.getFile()); 256 | 257 | request = new LocalMetadataRequest(metadata, repository, "different"); 258 | result = manager.find(session, request); 259 | assertNull(result.getFile()); 260 | } 261 | 262 | @Test 263 | public void testFindArtifactUsesTimestampedVersion() throws Exception { 264 | Artifact artifact = new DefaultArtifact("g.i.d:a.i.d:1.0-SNAPSHOT"); 265 | File file = new File(basedir, manager.getPathForLocalArtifact(artifact)); 266 | TestFileUtils.write("test", file); 267 | addLocalArtifact(artifact); 268 | 269 | artifact = artifact.setVersion("1.0-20110329.221805-4"); 270 | LocalArtifactRequest request = new LocalArtifactRequest(); 271 | request.setArtifact(artifact); 272 | LocalArtifactResult result = manager.find(session, request); 273 | assertNull(result.toString(), result.getFile()); 274 | assertFalse(result.toString(), result.isAvailable()); 275 | } 276 | 277 | } 278 | -------------------------------------------------------------------------------- /src/test/java/io/takari/aether/localrepo/TakariUpdateCheckManagerTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2010, 2013 Sonatype, Inc. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the Eclipse Public License v1.0 5 | * which accompanies this distribution, and is available at 6 | * http://www.eclipse.org/legal/epl-v10.html 7 | * 8 | * Contributors: 9 | * Sonatype, Inc. - initial API and implementation 10 | *******************************************************************************/ 11 | package io.takari.aether.localrepo; 12 | 13 | import static org.junit.Assert.assertEquals; 14 | import static org.junit.Assert.assertFalse; 15 | import static org.junit.Assert.assertNotNull; 16 | import static org.junit.Assert.assertNull; 17 | import static org.junit.Assert.assertTrue; 18 | 19 | import java.io.File; 20 | import java.net.URI; 21 | import java.util.Calendar; 22 | import java.util.Date; 23 | import java.util.TimeZone; 24 | 25 | import org.eclipse.aether.DefaultRepositorySystemSession; 26 | import org.eclipse.aether.RepositorySystemSession; 27 | import org.eclipse.aether.artifact.Artifact; 28 | import org.eclipse.aether.artifact.DefaultArtifact; 29 | import org.eclipse.aether.impl.UpdateCheck; 30 | import org.eclipse.aether.impl.UpdateCheckManager; 31 | import org.eclipse.aether.internal.impl.DefaultUpdatePolicyAnalyzer; 32 | import org.eclipse.aether.internal.test.util.TestUtils; 33 | import org.eclipse.aether.metadata.DefaultMetadata; 34 | import org.eclipse.aether.metadata.Metadata; 35 | import org.eclipse.aether.repository.RemoteRepository; 36 | import org.eclipse.aether.repository.RepositoryPolicy; 37 | import org.eclipse.aether.transfer.ArtifactNotFoundException; 38 | import org.eclipse.aether.transfer.ArtifactTransferException; 39 | import org.eclipse.aether.transfer.MetadataNotFoundException; 40 | import org.eclipse.aether.transfer.MetadataTransferException; 41 | import org.junit.After; 42 | import org.junit.Before; 43 | import org.junit.Test; 44 | 45 | /** 46 | */ 47 | public class TakariUpdateCheckManagerTest { 48 | 49 | private static final int HOUR = 60 * 60 * 1000; 50 | 51 | private UpdateCheckManager manager; 52 | 53 | private DefaultRepositorySystemSession session; 54 | 55 | private Metadata metadata; 56 | 57 | private RemoteRepository repository; 58 | 59 | private Artifact artifact; 60 | 61 | @Before 62 | public void setup() throws Exception { 63 | File dir = TestFileUtils.createTempFile(""); 64 | TestFileUtils.delete(dir); 65 | 66 | File metadataFile = new File(dir, "metadata.txt"); 67 | TestFileUtils.write("metadata", metadataFile); 68 | File artifactFile = new File(dir, "artifact.txt"); 69 | TestFileUtils.write("artifact", artifactFile); 70 | 71 | session = TestUtils.newSession(); 72 | repository = new RemoteRepository.Builder("id", "default", TestFileUtils.createTempDir().toURI().toURL().toString()).build(); 73 | manager = new TakariUpdateCheckManager().setUpdatePolicyAnalyzer(new DefaultUpdatePolicyAnalyzer()); 74 | metadata = new DefaultMetadata("gid", "aid", "ver", "maven-metadata.xml", Metadata.Nature.RELEASE_OR_SNAPSHOT, metadataFile); 75 | artifact = new DefaultArtifact("gid", "aid", "", "ext", "ver").setFile(artifactFile); 76 | } 77 | 78 | @After 79 | public void teardown() throws Exception { 80 | new File(metadata.getFile().getParent(), "resolver-status.properties").delete(); 81 | new File(artifact.getFile().getPath() + ".lastUpdated").delete(); 82 | metadata.getFile().delete(); 83 | artifact.getFile().delete(); 84 | TestFileUtils.delete(new File(new URI(repository.getUrl()))); 85 | } 86 | 87 | static void resetSessionData(RepositorySystemSession session) { 88 | session.getData().set("updateCheckManager.checks", null); 89 | } 90 | 91 | private UpdateCheck newMetadataCheck() { 92 | UpdateCheck check = new UpdateCheck(); 93 | check.setItem(metadata); 94 | check.setFile(metadata.getFile()); 95 | check.setRepository(repository); 96 | check.setAuthoritativeRepository(repository); 97 | check.setPolicy(RepositoryPolicy.UPDATE_POLICY_INTERVAL + ":10"); 98 | return check; 99 | } 100 | 101 | private UpdateCheck newArtifactCheck() { 102 | UpdateCheck check = new UpdateCheck(); 103 | check.setItem(artifact); 104 | check.setFile(artifact.getFile()); 105 | check.setRepository(repository); 106 | check.setPolicy(RepositoryPolicy.UPDATE_POLICY_INTERVAL + ":10"); 107 | return check; 108 | } 109 | 110 | @Test(expected = Exception.class) 111 | public void testCheckMetadataFailOnNoFile() throws Exception { 112 | UpdateCheck check = newMetadataCheck(); 113 | check.setItem(metadata.setFile(null)); 114 | check.setFile(null); 115 | 116 | manager.checkMetadata(session, check); 117 | } 118 | 119 | @Test 120 | public void testCheckMetadataUpdatePolicyRequired() throws Exception { 121 | UpdateCheck check = newMetadataCheck(); 122 | 123 | Calendar cal = Calendar.getInstance(); 124 | cal.add(Calendar.DATE, -1); 125 | check.setLocalLastUpdated(cal.getTimeInMillis()); 126 | 127 | check.setPolicy(RepositoryPolicy.UPDATE_POLICY_ALWAYS); 128 | manager.checkMetadata(session, check); 129 | assertNull(check.getException()); 130 | assertTrue(check.isRequired()); 131 | 132 | check.setPolicy(RepositoryPolicy.UPDATE_POLICY_DAILY); 133 | manager.checkMetadata(session, check); 134 | assertNull(check.getException()); 135 | assertTrue(check.isRequired()); 136 | 137 | check.setPolicy(RepositoryPolicy.UPDATE_POLICY_INTERVAL + ":60"); 138 | manager.checkMetadata(session, check); 139 | assertNull(check.getException()); 140 | assertTrue(check.isRequired()); 141 | } 142 | 143 | @Test 144 | public void testCheckMetadataUpdatePolicyNotRequired() throws Exception { 145 | UpdateCheck check = newMetadataCheck(); 146 | 147 | check.setLocalLastUpdated(System.currentTimeMillis()); 148 | 149 | check.setPolicy(RepositoryPolicy.UPDATE_POLICY_NEVER); 150 | manager.checkMetadata(session, check); 151 | assertFalse(check.isRequired()); 152 | 153 | check.setPolicy(RepositoryPolicy.UPDATE_POLICY_DAILY); 154 | manager.checkMetadata(session, check); 155 | assertFalse(check.isRequired()); 156 | 157 | check.setPolicy(RepositoryPolicy.UPDATE_POLICY_INTERVAL + ":61"); 158 | manager.checkMetadata(session, check); 159 | assertFalse(check.isRequired()); 160 | 161 | check.setPolicy("no particular policy"); 162 | manager.checkMetadata(session, check); 163 | assertFalse(check.isRequired()); 164 | } 165 | 166 | @Test 167 | public void testCheckMetadata() throws Exception { 168 | UpdateCheck check = newMetadataCheck(); 169 | check.setPolicy(RepositoryPolicy.UPDATE_POLICY_DAILY); 170 | 171 | // existing file, never checked before 172 | manager.checkMetadata(session, check); 173 | assertEquals(true, check.isRequired()); 174 | 175 | // just checked 176 | manager.touchMetadata(session, check); 177 | resetSessionData(session); 178 | 179 | check = newMetadataCheck(); 180 | check.setPolicy(RepositoryPolicy.UPDATE_POLICY_INTERVAL + ":60"); 181 | 182 | manager.checkMetadata(session, check); 183 | assertEquals(false, check.isRequired()); 184 | 185 | // no local file 186 | check.getFile().delete(); 187 | manager.checkMetadata(session, check); 188 | assertEquals(true, check.isRequired()); 189 | // (! file.exists && ! repoKey) -> no timestamp 190 | } 191 | 192 | @Test 193 | public void testCheckMetadataNoLocalFile() throws Exception { 194 | metadata.getFile().delete(); 195 | 196 | UpdateCheck check = newMetadataCheck(); 197 | 198 | long lastUpdate = new Date().getTime() - HOUR; 199 | check.setLocalLastUpdated(lastUpdate); 200 | 201 | // ! file.exists && updateRequired -> check in remote repo 202 | check.setLocalLastUpdated(lastUpdate); 203 | manager.checkMetadata(session, check); 204 | assertEquals(true, check.isRequired()); 205 | } 206 | 207 | @Test 208 | public void testCheckMetadataNotFoundInRepoCachingEnabled() throws Exception { 209 | metadata.getFile().delete(); 210 | session.setResolutionErrorPolicy(new SimpleResolutionErrorPolicy(true, false)); 211 | 212 | UpdateCheck check = newMetadataCheck(); 213 | 214 | check.setException(new MetadataNotFoundException(metadata, repository, "")); 215 | manager.touchMetadata(session, check); 216 | resetSessionData(session); 217 | 218 | // ! file.exists && ! updateRequired -> artifact not found in remote repo 219 | check = newMetadataCheck().setPolicy(RepositoryPolicy.UPDATE_POLICY_DAILY); 220 | manager.checkMetadata(session, check); 221 | assertEquals(false, check.isRequired()); 222 | assertNotNull(check.getException()); 223 | } 224 | 225 | @Test 226 | public void testCheckMetadataNotFoundInRepoCachingDisabled() throws Exception { 227 | metadata.getFile().delete(); 228 | session.setResolutionErrorPolicy(new SimpleResolutionErrorPolicy(false, false)); 229 | 230 | UpdateCheck check = newMetadataCheck(); 231 | 232 | check.setException(new MetadataNotFoundException(metadata, repository, "")); 233 | manager.touchMetadata(session, check); 234 | resetSessionData(session); 235 | 236 | // ! file.exists && updateRequired -> check in remote repo 237 | check = newMetadataCheck().setPolicy(RepositoryPolicy.UPDATE_POLICY_DAILY); 238 | manager.checkMetadata(session, check); 239 | assertEquals(true, check.isRequired()); 240 | assertNull(check.getException()); 241 | } 242 | 243 | @Test 244 | public void testCheckMetadataErrorFromRepo() throws Exception { 245 | metadata.getFile().delete(); 246 | 247 | UpdateCheck check = newMetadataCheck(); 248 | check.setPolicy(RepositoryPolicy.UPDATE_POLICY_DAILY); 249 | 250 | check.setException(new MetadataTransferException(metadata, repository, "some error")); 251 | manager.touchMetadata(session, check); 252 | resetSessionData(session); 253 | 254 | // ! file.exists && ! updateRequired && previousError -> depends on transfer error caching 255 | check = newMetadataCheck(); 256 | session.setResolutionErrorPolicy(new SimpleResolutionErrorPolicy(false, true)); 257 | manager.checkMetadata(session, check); 258 | assertEquals(false, check.isRequired()); 259 | assertTrue(String.valueOf(check.getException()), check.getException().getMessage().contains("some error")); 260 | } 261 | 262 | @Test 263 | public void testCheckMetadataErrorFromRepoNoCaching() throws Exception { 264 | metadata.getFile().delete(); 265 | 266 | UpdateCheck check = newMetadataCheck(); 267 | check.setPolicy(RepositoryPolicy.UPDATE_POLICY_DAILY); 268 | 269 | check.setException(new MetadataTransferException(metadata, repository, "some error")); 270 | manager.touchMetadata(session, check); 271 | resetSessionData(session); 272 | 273 | // ! file.exists && ! updateRequired && previousError -> depends on transfer error caching 274 | check = newMetadataCheck(); 275 | session.setResolutionErrorPolicy(new SimpleResolutionErrorPolicy(false, false)); 276 | manager.checkMetadata(session, check); 277 | assertEquals(true, check.isRequired()); 278 | assertNull(check.getException()); 279 | } 280 | 281 | @Test 282 | public void testCheckMetadataAtMostOnceDuringSessionEvenIfUpdatePolicyAlways() throws Exception { 283 | UpdateCheck check = newMetadataCheck(); 284 | check.setPolicy(RepositoryPolicy.UPDATE_POLICY_ALWAYS); 285 | 286 | // first check 287 | manager.checkMetadata(session, check); 288 | assertEquals(true, check.isRequired()); 289 | 290 | manager.touchMetadata(session, check); 291 | 292 | // second check in same session 293 | manager.checkMetadata(session, check); 294 | assertEquals(false, check.isRequired()); 295 | } 296 | 297 | @Test 298 | public void testCheckMetadataWhenLocallyMissingEvenIfUpdatePolicyIsNever() throws Exception { 299 | UpdateCheck check = newMetadataCheck(); 300 | check.setPolicy(RepositoryPolicy.UPDATE_POLICY_NEVER); 301 | session.setResolutionErrorPolicy(new SimpleResolutionErrorPolicy(true, false)); 302 | 303 | check.getFile().delete(); 304 | assertEquals(check.getFile().getAbsolutePath(), false, check.getFile().exists()); 305 | 306 | manager.checkMetadata(session, check); 307 | assertEquals(true, check.isRequired()); 308 | } 309 | 310 | @Test 311 | public void testCheckMetadataWhenLocallyPresentButInvalidEvenIfUpdatePolicyIsNever() throws Exception { 312 | UpdateCheck check = newMetadataCheck(); 313 | check.setPolicy(RepositoryPolicy.UPDATE_POLICY_NEVER); 314 | session.setResolutionErrorPolicy(new SimpleResolutionErrorPolicy(true, false)); 315 | 316 | manager.touchMetadata(session, check); 317 | resetSessionData(session); 318 | 319 | check.setFileValid(false); 320 | 321 | manager.checkMetadata(session, check); 322 | assertEquals(true, check.isRequired()); 323 | } 324 | 325 | @Test 326 | public void testCheckMetadataWhenLocallyDeletedEvenIfTimestampUpToDate() throws Exception { 327 | UpdateCheck check = newMetadataCheck(); 328 | session.setResolutionErrorPolicy(new SimpleResolutionErrorPolicy(true, false)); 329 | 330 | manager.touchMetadata(session, check); 331 | resetSessionData(session); 332 | 333 | check.getFile().delete(); 334 | assertEquals(check.getFile().getAbsolutePath(), false, check.getFile().exists()); 335 | 336 | manager.checkMetadata(session, check); 337 | assertEquals(true, check.isRequired()); 338 | } 339 | 340 | @Test 341 | public void testCheckMetadataNotWhenUpdatePolicyIsNeverAndTimestampIsUnavailable() throws Exception { 342 | UpdateCheck check = newMetadataCheck(); 343 | check.setPolicy(RepositoryPolicy.UPDATE_POLICY_NEVER); 344 | session.setResolutionErrorPolicy(new SimpleResolutionErrorPolicy(true, false)); 345 | 346 | manager.checkMetadata(session, check); 347 | assertEquals(false, check.isRequired()); 348 | } 349 | 350 | @Test(expected = IllegalArgumentException.class) 351 | public void testCheckArtifactFailOnNoFile() throws Exception { 352 | UpdateCheck check = newArtifactCheck(); 353 | check.setItem(artifact.setFile(null)); 354 | check.setFile(null); 355 | 356 | manager.checkArtifact(session, check); 357 | assertNotNull(check.getException()); 358 | } 359 | 360 | @Test 361 | public void testCheckArtifactUpdatePolicyRequired() throws Exception { 362 | UpdateCheck check = newArtifactCheck(); 363 | check.setItem(artifact); 364 | check.setFile(artifact.getFile()); 365 | 366 | Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); 367 | cal.add(Calendar.DATE, -1); 368 | long lastUpdate = cal.getTimeInMillis(); 369 | artifact.getFile().setLastModified(lastUpdate); 370 | check.setLocalLastUpdated(lastUpdate); 371 | 372 | check.setPolicy(RepositoryPolicy.UPDATE_POLICY_ALWAYS); 373 | manager.checkArtifact(session, check); 374 | assertNull(check.getException()); 375 | assertTrue(check.isRequired()); 376 | 377 | check.setPolicy(RepositoryPolicy.UPDATE_POLICY_DAILY); 378 | manager.checkArtifact(session, check); 379 | assertNull(check.getException()); 380 | assertTrue(check.isRequired()); 381 | 382 | check.setPolicy(RepositoryPolicy.UPDATE_POLICY_INTERVAL + ":60"); 383 | manager.checkArtifact(session, check); 384 | assertNull(check.getException()); 385 | assertTrue(check.isRequired()); 386 | } 387 | 388 | @Test 389 | public void testCheckArtifactUpdatePolicyNotRequired() throws Exception { 390 | UpdateCheck check = newArtifactCheck(); 391 | check.setItem(artifact); 392 | check.setFile(artifact.getFile()); 393 | 394 | Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); 395 | cal.add(Calendar.HOUR_OF_DAY, -1); 396 | check.setLocalLastUpdated(cal.getTimeInMillis()); 397 | 398 | check.setPolicy(RepositoryPolicy.UPDATE_POLICY_NEVER); 399 | manager.checkArtifact(session, check); 400 | assertFalse(check.isRequired()); 401 | 402 | check.setPolicy(RepositoryPolicy.UPDATE_POLICY_DAILY); 403 | manager.checkArtifact(session, check); 404 | assertFalse(check.isRequired()); 405 | 406 | check.setPolicy(RepositoryPolicy.UPDATE_POLICY_INTERVAL + ":61"); 407 | manager.checkArtifact(session, check); 408 | assertFalse(check.isRequired()); 409 | 410 | check.setPolicy("no particular policy"); 411 | manager.checkArtifact(session, check); 412 | assertFalse(check.isRequired()); 413 | } 414 | 415 | @Test 416 | public void testCheckArtifact() throws Exception { 417 | UpdateCheck check = newArtifactCheck(); 418 | long fifteenMinutes = new Date().getTime() - (15 * 60 * 1000); 419 | check.getFile().setLastModified(fifteenMinutes); 420 | // time is truncated on setLastModfied 421 | fifteenMinutes = check.getFile().lastModified(); 422 | 423 | // never checked before 424 | manager.checkArtifact(session, check); 425 | assertEquals(true, check.isRequired()); 426 | 427 | // just checked 428 | check.setLocalLastUpdated(0); 429 | long lastUpdate = new Date().getTime(); 430 | check.getFile().setLastModified(lastUpdate); 431 | lastUpdate = check.getFile().lastModified(); 432 | 433 | manager.checkArtifact(session, check); 434 | assertEquals(false, check.isRequired()); 435 | 436 | // no local file, no repo timestamp 437 | check.setLocalLastUpdated(0); 438 | check.getFile().delete(); 439 | manager.checkArtifact(session, check); 440 | assertEquals(true, check.isRequired()); 441 | } 442 | 443 | @Test 444 | public void testCheckArtifactNoLocalFile() throws Exception { 445 | artifact.getFile().delete(); 446 | UpdateCheck check = newArtifactCheck(); 447 | 448 | long lastUpdate = new Date().getTime() - HOUR; 449 | 450 | // ! file.exists && updateRequired -> check in remote repo 451 | check.setLocalLastUpdated(lastUpdate); 452 | manager.checkArtifact(session, check); 453 | assertEquals(true, check.isRequired()); 454 | } 455 | 456 | @Test 457 | public void testCheckArtifactNotFoundInRepoCachingEnabled() throws Exception { 458 | artifact.getFile().delete(); 459 | session.setResolutionErrorPolicy(new SimpleResolutionErrorPolicy(true, false)); 460 | 461 | UpdateCheck check = newArtifactCheck(); 462 | check.setException(new ArtifactNotFoundException(artifact, repository)); 463 | manager.touchArtifact(session, check); 464 | resetSessionData(session); 465 | 466 | // ! file.exists && ! updateRequired -> artifact not found in remote repo 467 | check = newArtifactCheck().setPolicy(RepositoryPolicy.UPDATE_POLICY_DAILY); 468 | manager.checkArtifact(session, check); 469 | assertEquals(false, check.isRequired()); 470 | assertTrue(check.getException() instanceof ArtifactNotFoundException); 471 | } 472 | 473 | @Test 474 | public void testCheckArtifactNotFoundInRepoCachingDisabled() throws Exception { 475 | artifact.getFile().delete(); 476 | session.setResolutionErrorPolicy(new SimpleResolutionErrorPolicy(false, false)); 477 | 478 | UpdateCheck check = newArtifactCheck(); 479 | check.setException(new ArtifactNotFoundException(artifact, repository)); 480 | manager.touchArtifact(session, check); 481 | resetSessionData(session); 482 | 483 | // ! file.exists && updateRequired -> check in remote repo 484 | check = newArtifactCheck().setPolicy(RepositoryPolicy.UPDATE_POLICY_DAILY); 485 | manager.checkArtifact(session, check); 486 | assertEquals(true, check.isRequired()); 487 | assertNull(check.getException()); 488 | } 489 | 490 | @Test 491 | public void testCheckArtifactErrorFromRepoCachingEnabled() throws Exception { 492 | artifact.getFile().delete(); 493 | 494 | UpdateCheck check = newArtifactCheck(); 495 | check.setPolicy(RepositoryPolicy.UPDATE_POLICY_DAILY); 496 | check.setException(new ArtifactTransferException(artifact, repository, "some error")); 497 | manager.touchArtifact(session, check); 498 | resetSessionData(session); 499 | 500 | // ! file.exists && ! updateRequired && previousError -> depends on transfer error caching 501 | check = newArtifactCheck(); 502 | session.setResolutionErrorPolicy(new SimpleResolutionErrorPolicy(false, true)); 503 | manager.checkArtifact(session, check); 504 | assertEquals(false, check.isRequired()); 505 | } 506 | 507 | @Test 508 | public void testCheckArtifactErrorFromRepoCachingDisabled() throws Exception { 509 | artifact.getFile().delete(); 510 | 511 | UpdateCheck check = newArtifactCheck(); 512 | check.setPolicy(RepositoryPolicy.UPDATE_POLICY_DAILY); 513 | check.setException(new ArtifactTransferException(artifact, repository, "some error")); 514 | manager.touchArtifact(session, check); 515 | resetSessionData(session); 516 | 517 | // ! file.exists && ! updateRequired && previousError -> depends on transfer error caching 518 | check = newArtifactCheck(); 519 | session.setResolutionErrorPolicy(new SimpleResolutionErrorPolicy(false, false)); 520 | manager.checkArtifact(session, check); 521 | assertEquals(true, check.isRequired()); 522 | assertNull(check.getException()); 523 | } 524 | 525 | @Test 526 | public void testCheckArtifactAtMostOnceDuringSessionEvenIfUpdatePolicyAlways() throws Exception { 527 | UpdateCheck check = newArtifactCheck(); 528 | check.setPolicy(RepositoryPolicy.UPDATE_POLICY_ALWAYS); 529 | 530 | // first check 531 | manager.checkArtifact(session, check); 532 | assertEquals(true, check.isRequired()); 533 | 534 | manager.touchArtifact(session, check); 535 | 536 | // second check in same session 537 | manager.checkArtifact(session, check); 538 | assertEquals(false, check.isRequired()); 539 | } 540 | 541 | @Test 542 | public void testCheckArtifactWhenLocallyMissingEvenIfUpdatePolicyIsNever() throws Exception { 543 | UpdateCheck check = newArtifactCheck(); 544 | check.setPolicy(RepositoryPolicy.UPDATE_POLICY_NEVER); 545 | session.setResolutionErrorPolicy(new SimpleResolutionErrorPolicy(true, false)); 546 | 547 | check.getFile().delete(); 548 | assertEquals(check.getFile().getAbsolutePath(), false, check.getFile().exists()); 549 | 550 | manager.checkArtifact(session, check); 551 | assertEquals(true, check.isRequired()); 552 | } 553 | 554 | @Test 555 | public void testCheckArtifactWhenLocallyPresentButInvalidEvenIfUpdatePolicyIsNever() throws Exception { 556 | UpdateCheck check = newArtifactCheck(); 557 | check.setPolicy(RepositoryPolicy.UPDATE_POLICY_NEVER); 558 | session.setResolutionErrorPolicy(new SimpleResolutionErrorPolicy(true, false)); 559 | 560 | manager.touchArtifact(session, check); 561 | resetSessionData(session); 562 | 563 | check.setFileValid(false); 564 | 565 | manager.checkArtifact(session, check); 566 | assertEquals(true, check.isRequired()); 567 | } 568 | 569 | @Test 570 | public void testCheckArtifactWhenLocallyDeletedEvenIfTimestampUpToDate() throws Exception { 571 | UpdateCheck check = newArtifactCheck(); 572 | session.setResolutionErrorPolicy(new SimpleResolutionErrorPolicy(true, false)); 573 | 574 | manager.touchArtifact(session, check); 575 | resetSessionData(session); 576 | 577 | check.getFile().delete(); 578 | assertEquals(check.getFile().getAbsolutePath(), false, check.getFile().exists()); 579 | 580 | manager.checkArtifact(session, check); 581 | assertEquals(true, check.isRequired()); 582 | } 583 | 584 | @Test 585 | public void testCheckArtifactNotWhenUpdatePolicyIsNeverAndTimestampIsUnavailable() throws Exception { 586 | UpdateCheck check = newArtifactCheck(); 587 | check.setPolicy(RepositoryPolicy.UPDATE_POLICY_NEVER); 588 | session.setResolutionErrorPolicy(new SimpleResolutionErrorPolicy(true, false)); 589 | 590 | manager.checkArtifact(session, check); 591 | assertEquals(false, check.isRequired()); 592 | } 593 | 594 | } 595 | -------------------------------------------------------------------------------- /src/test/java/io/takari/aether/localrepo/TestFileUtils.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2010, 2013 Sonatype, Inc. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the Eclipse Public License v1.0 5 | * which accompanies this distribution, and is available at 6 | * http://www.eclipse.org/legal/epl-v10.html 7 | * 8 | * Contributors: 9 | * Sonatype, Inc. - initial API and implementation 10 | *******************************************************************************/ 11 | package io.takari.aether.localrepo; 12 | 13 | import java.io.BufferedOutputStream; 14 | import java.io.Closeable; 15 | import java.io.File; 16 | import java.io.FileInputStream; 17 | import java.io.FileOutputStream; 18 | import java.io.IOException; 19 | import java.io.OutputStream; 20 | import java.io.RandomAccessFile; 21 | import java.util.ArrayList; 22 | import java.util.Collection; 23 | import java.util.Properties; 24 | import java.util.UUID; 25 | 26 | import org.junit.Assert; 27 | 28 | public class TestFileUtils { 29 | 30 | private static final File TMP = new File(System.getProperty("java.io.tmpdir"), "aether-" + UUID.randomUUID().toString().substring(0, 8)); 31 | 32 | static { 33 | Runtime.getRuntime().addShutdownHook(new Thread() { 34 | @Override 35 | public void run() { 36 | try { 37 | delete(TMP); 38 | } catch (IOException e) { 39 | e.printStackTrace(); 40 | } 41 | } 42 | }); 43 | } 44 | 45 | private TestFileUtils() { 46 | // hide constructor 47 | } 48 | 49 | public static void deleteTempFiles() throws IOException { 50 | delete(TMP); 51 | } 52 | 53 | public static File createTempFile(String contents) throws IOException { 54 | return createTempFile(contents.getBytes("UTF-8"), 1); 55 | } 56 | 57 | public static File createTempFile(byte[] pattern, int repeat) throws IOException { 58 | mkdirs(TMP); 59 | File tmpFile = File.createTempFile("tmpfile-", ".data", TMP); 60 | write(pattern, repeat, tmpFile); 61 | 62 | return tmpFile; 63 | } 64 | 65 | public static void write(String content, File file) throws IOException { 66 | write(content.getBytes("UTF-8"), 1, file); 67 | } 68 | 69 | public static void write(byte[] pattern, int repeat, File file) throws IOException { 70 | file.deleteOnExit(); 71 | file.getParentFile().mkdirs(); 72 | OutputStream out = null; 73 | try { 74 | out = new BufferedOutputStream(new FileOutputStream(file)); 75 | for (int i = 0; i < repeat; i++) { 76 | out.write(pattern); 77 | } 78 | } finally { 79 | close(out); 80 | } 81 | } 82 | 83 | public static long copy(File source, File target) throws IOException { 84 | long total = 0; 85 | 86 | FileInputStream fis = null; 87 | OutputStream fos = null; 88 | try { 89 | fis = new FileInputStream(source); 90 | 91 | mkdirs(target.getParentFile()); 92 | 93 | fos = new BufferedOutputStream(new FileOutputStream(target)); 94 | 95 | for (byte[] buffer = new byte[1024 * 32];;) { 96 | int bytes = fis.read(buffer); 97 | if (bytes < 0) { 98 | break; 99 | } 100 | 101 | fos.write(buffer, 0, bytes); 102 | 103 | total += bytes; 104 | } 105 | } finally { 106 | close(fis); 107 | close(fos); 108 | } 109 | 110 | return total; 111 | } 112 | 113 | private static void close(Closeable c) throws IOException { 114 | if (c != null) { 115 | try { 116 | c.close(); 117 | } catch (IOException e) { 118 | // ignore 119 | } 120 | } 121 | } 122 | 123 | public static void delete(File file) throws IOException { 124 | if (file == null) { 125 | return; 126 | } 127 | 128 | Collection undeletables = new ArrayList(); 129 | 130 | delete(file, undeletables); 131 | 132 | if (!undeletables.isEmpty()) { 133 | throw new IOException("Failed to delete " + undeletables); 134 | } 135 | } 136 | 137 | private static void delete(File file, Collection undeletables) { 138 | String[] children = file.list(); 139 | if (children != null) { 140 | for (String child : children) { 141 | delete(new File(file, child), undeletables); 142 | } 143 | } 144 | 145 | if (!del(file)) { 146 | undeletables.add(file.getAbsoluteFile()); 147 | } 148 | } 149 | 150 | private static boolean del(File file) { 151 | for (int i = 0; i < 10; i++) { 152 | if (file.delete() || !file.exists()) { 153 | return true; 154 | } 155 | } 156 | return false; 157 | } 158 | 159 | public static byte[] getContent(File file) throws IOException { 160 | RandomAccessFile in = null; 161 | try { 162 | in = new RandomAccessFile(file, "r"); 163 | byte[] actual = new byte[(int) in.length()]; 164 | in.readFully(actual); 165 | return actual; 166 | } finally { 167 | close(in); 168 | } 169 | } 170 | 171 | public static void assertContent(byte[] expected, File file) throws IOException { 172 | Assert.assertArrayEquals(expected, getContent(file)); 173 | } 174 | 175 | public static void assertContent(String expected, File file) throws IOException { 176 | byte[] content = getContent(file); 177 | String msg = new String(content, "UTF-8"); 178 | if (msg.length() > 10) { 179 | msg = msg.substring(0, 10) + "..."; 180 | } 181 | Assert.assertArrayEquals("content was '" + msg + "'\n", expected.getBytes("UTF-8"), content); 182 | } 183 | 184 | public static boolean mkdirs(File directory) { 185 | if (directory == null) { 186 | return false; 187 | } 188 | 189 | if (directory.exists()) { 190 | return false; 191 | } 192 | if (directory.mkdir()) { 193 | return true; 194 | } 195 | 196 | File canonDir = null; 197 | try { 198 | canonDir = directory.getCanonicalFile(); 199 | } catch (IOException e) { 200 | return false; 201 | } 202 | 203 | File parentDir = canonDir.getParentFile(); 204 | return (parentDir != null && (mkdirs(parentDir) || parentDir.exists()) && canonDir.mkdir()); 205 | } 206 | 207 | public static File createTempDir() throws IOException { 208 | return createTempDir(""); 209 | } 210 | 211 | public static File createTempDir(String suffix) throws IOException { 212 | mkdirs(TMP); 213 | 214 | File tmpFile = File.createTempFile("tmpdir-", suffix, TMP); 215 | 216 | delete(tmpFile); 217 | mkdirs(tmpFile); 218 | 219 | return tmpFile; 220 | } 221 | 222 | public static void read(Properties props, File file) throws IOException { 223 | FileInputStream fis = null; 224 | try { 225 | fis = new FileInputStream(file); 226 | props.load(fis); 227 | } finally { 228 | close(fis); 229 | } 230 | } 231 | 232 | public static void write(Properties props, File file) throws IOException { 233 | file.getParentFile().mkdirs(); 234 | 235 | FileOutputStream fos = null; 236 | try { 237 | fos = new FileOutputStream(file); 238 | props.store(fos, "aether-test"); 239 | } finally { 240 | close(fos); 241 | } 242 | } 243 | 244 | } 245 | -------------------------------------------------------------------------------- /src/test/java/io/takari/aether/localrepo/TrackingFileManagerTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2010, 2011 Sonatype, Inc. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the Eclipse Public License v1.0 5 | * which accompanies this distribution, and is available at 6 | * http://www.eclipse.org/legal/epl-v10.html 7 | * 8 | * Contributors: 9 | * Sonatype, Inc. - initial API and implementation 10 | *******************************************************************************/ 11 | package io.takari.aether.localrepo; 12 | 13 | import static org.junit.Assert.assertEquals; 14 | import static org.junit.Assert.assertNotNull; 15 | import static org.junit.Assert.assertNull; 16 | import static org.junit.Assert.assertTrue; 17 | 18 | import java.io.File; 19 | import java.util.ArrayList; 20 | import java.util.Collections; 21 | import java.util.HashMap; 22 | import java.util.List; 23 | import java.util.Map; 24 | import java.util.Properties; 25 | 26 | import org.eclipse.aether.internal.test.util.TestFileUtils; 27 | import org.junit.Test; 28 | 29 | public class TrackingFileManagerTest { 30 | 31 | @Test 32 | public void testRead() throws Exception { 33 | TrackingFileManager tfm = new TrackingFileManager(); 34 | 35 | File propFile = TestFileUtils.createTempFile("#COMMENT\nkey1=value1\nkey2 : value2"); 36 | Properties props = tfm.read(propFile); 37 | 38 | assertNotNull(props); 39 | assertEquals(String.valueOf(props), 2, props.size()); 40 | assertEquals("value1", props.get("key1")); 41 | assertEquals("value2", props.get("key2")); 42 | 43 | assertTrue("Leaked file: " + propFile, propFile.delete()); 44 | 45 | props = tfm.read(propFile); 46 | assertNull(String.valueOf(props), props); 47 | } 48 | 49 | @Test 50 | public void testReadNoFileLeak() throws Exception { 51 | TrackingFileManager tfm = new TrackingFileManager(); 52 | 53 | for (int i = 0; i < 1000; i++) { 54 | File propFile = TestFileUtils.createTempFile("#COMMENT\nkey1=value1\nkey2 : value2"); 55 | assertNotNull(tfm.read(propFile)); 56 | assertTrue("Leaked file: " + propFile, propFile.delete()); 57 | } 58 | } 59 | 60 | @Test 61 | public void testUpdate() throws Exception { 62 | TrackingFileManager tfm = new TrackingFileManager(); 63 | 64 | // NOTE: The excessive repetitions are to check the update properly truncates the file 65 | File propFile = TestFileUtils.createTempFile("key1=value1\nkey2 : value2\n".getBytes("UTF-8"), 1000); 66 | 67 | Map updates = new HashMap(); 68 | updates.put("key1", "v"); 69 | updates.put("key2", null); 70 | 71 | tfm.update(propFile, updates); 72 | 73 | Properties props = tfm.read(propFile); 74 | 75 | assertNotNull(props); 76 | assertEquals(String.valueOf(props), 1, props.size()); 77 | assertEquals("v", props.get("key1")); 78 | assertNull(String.valueOf(props.get("key2")), props.get("key2")); 79 | } 80 | 81 | @Test 82 | public void testUpdateNoFileLeak() throws Exception { 83 | TrackingFileManager tfm = new TrackingFileManager(); 84 | 85 | Map updates = new HashMap(); 86 | updates.put("k", "v"); 87 | 88 | for (int i = 0; i < 1000; i++) { 89 | File propFile = TestFileUtils.createTempFile("#COMMENT\nkey1=value1\nkey2 : value2"); 90 | assertNotNull(tfm.update(propFile, updates)); 91 | assertTrue("Leaked file: " + propFile, propFile.delete()); 92 | } 93 | } 94 | 95 | @Test 96 | public void testLockingOnCanonicalPath() throws Exception { 97 | final TrackingFileManager tfm = new TrackingFileManager(); 98 | 99 | final File propFile = TestFileUtils.createTempFile("#COMMENT\nkey1=value1\nkey2 : value2"); 100 | 101 | final List errors = Collections.synchronizedList(new ArrayList()); 102 | 103 | Thread[] threads = new Thread[4]; 104 | for (int i = 0; i < threads.length; i++) { 105 | String path = propFile.getParent(); 106 | for (int j = 0; j < i; j++) { 107 | path += "/."; 108 | } 109 | path += "/" + propFile.getName(); 110 | final File file = new File(path); 111 | 112 | threads[i] = new Thread() { 113 | public void run() { 114 | try { 115 | for (int i = 0; i < 1000; i++) { 116 | assertNotNull(tfm.read(file)); 117 | } 118 | } catch (Throwable e) { 119 | errors.add(e); 120 | } 121 | } 122 | }; 123 | } 124 | 125 | for (int i = 0; i < threads.length; i++) { 126 | threads[i].start(); 127 | } 128 | 129 | for (int i = 0; i < threads.length; i++) { 130 | threads[i].join(); 131 | } 132 | 133 | assertEquals(Collections.emptyList(), errors); 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /src/test/java/io/takari/aether/localrepo/its/TakariLocalRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package io.takari.aether.localrepo.its; 2 | 3 | import static org.junit.Assert.assertFalse; 4 | import io.takari.maven.testing.TestProperties; 5 | import io.takari.maven.testing.TestResources; 6 | import io.takari.maven.testing.executor.MavenExecution; 7 | import io.takari.maven.testing.executor.MavenExecutionResult; 8 | import io.takari.maven.testing.executor.MavenRuntime; 9 | import io.takari.maven.testing.executor.MavenRuntime.MavenRuntimeBuilder; 10 | import io.takari.maven.testing.executor.MavenVersions; 11 | import io.takari.maven.testing.executor.junit.MavenJUnitTestRunner; 12 | 13 | import java.io.File; 14 | 15 | import org.codehaus.plexus.util.FileUtils; 16 | import org.junit.Rule; 17 | import org.junit.Test; 18 | import org.junit.runner.RunWith; 19 | 20 | @RunWith(MavenJUnitTestRunner.class) 21 | @MavenVersions({"3.3.1", "3.3.3"}) 22 | public class TakariLocalRepositoryTest { 23 | 24 | @Rule 25 | public final TestResources resources = new TestResources(); 26 | public final TestProperties proprties = new TestProperties(); 27 | public final MavenRuntime verifier; 28 | private String basedir; 29 | 30 | public TakariLocalRepositoryTest(MavenRuntimeBuilder runtimeBuilder) throws Exception { 31 | this.verifier = runtimeBuilder.withExtension(new File("target/classes").getCanonicalFile()) // 32 | .build(); 33 | } 34 | 35 | @Test 36 | public void validateRetryOnDowloadErrorFlagIsFunctional() throws Exception { 37 | File localRepository = new File(getBasedir(), "target/local-repo"); 38 | FileUtils.deleteDirectory(localRepository); 39 | File basedir = resources.getBasedir("basic-it"); 40 | MavenExecution execution = verifier.forProject(basedir) // 41 | .withCliOptions(String.format("-Dmaven.repo.local=%s", localRepository.getAbsolutePath())) // 42 | .withCliOptions(String.format("-Dmaven.retryOnDownloadError=true")); 43 | MavenExecutionResult result = execution.execute("compile"); 44 | 45 | result.assertLogText("Could not resolve dependencies for project io.takari.aether.localrepo.its:update-check:jar:0.1.0"); 46 | 47 | File updateCheckFile = new File(localRepository, "io/takari/aether/localrepo/its/non-existent/1.0/non-existent-1.0.jar.lastUpdated"); 48 | assertFalse(updateCheckFile.exists()); 49 | } 50 | 51 | public final String getBasedir() { 52 | if (null == basedir) { 53 | basedir = System.getProperty("basedir", new File("").getAbsolutePath()); 54 | } 55 | return basedir; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/io/takari/filemanager/DefaultFileManagerTest.java: -------------------------------------------------------------------------------- 1 | package io.takari.filemanager; 2 | 3 | /******************************************************************************* 4 | * Copyright (c) 2010-2013 Sonatype, Inc. 5 | * All rights reserved. This program and the accompanying materials 6 | * are made available under the terms of the Eclipse Public License v1.0 7 | * which accompanies this distribution, and is available at 8 | * http://www.eclipse.org/legal/epl-v10.html 9 | *******************************************************************************/ 10 | 11 | import static org.junit.Assert.*; 12 | import io.takari.filemanager.Lock; 13 | import io.takari.filemanager.internal.DefaultFileManager; 14 | 15 | import java.io.File; 16 | import java.io.IOException; 17 | import java.nio.channels.FileChannel; 18 | import java.util.concurrent.atomic.AtomicReference; 19 | 20 | import org.junit.Before; 21 | import org.junit.Test; 22 | 23 | import edu.umd.cs.mtc.MultithreadedTestCase; 24 | import edu.umd.cs.mtc.TestFramework; 25 | 26 | @SuppressWarnings("unused") 27 | public class DefaultFileManagerTest { 28 | 29 | private DefaultFileManager manager; 30 | 31 | private Process process; 32 | 33 | private DefaultFileManager newManager() { 34 | return new DefaultFileManager(); 35 | } 36 | 37 | @Before 38 | public void setup() throws IOException { 39 | manager = newManager(); 40 | } 41 | 42 | @Test 43 | public void testExternalLockTryReadLock() throws InterruptedException, IOException { 44 | int wait = 1500; 45 | 46 | File file = TestFileUtils.createTempFile(""); 47 | 48 | Lock lock = manager.readLock(file); 49 | 50 | ExternalProcessFileLock ext = new ExternalProcessFileLock(file); 51 | process = ext.lockFile(wait); 52 | 53 | long start = ext.awaitLock(); 54 | 55 | lock.lock(); 56 | 57 | long end = System.currentTimeMillis(); 58 | 59 | lock.unlock(); 60 | 61 | String message = "expected " + wait + "ms wait, real delta: " + (end - start); 62 | 63 | assertTrue(message, end - start > wait); 64 | } 65 | 66 | @Test 67 | public void testExternalLockTryWriteLock() throws InterruptedException, IOException { 68 | int wait = 1500; 69 | 70 | File file = TestFileUtils.createTempFile(""); 71 | 72 | ExternalProcessFileLock ext = new ExternalProcessFileLock(file); 73 | process = ext.lockFile(wait); 74 | 75 | Lock lock = manager.writeLock(file); 76 | 77 | long start = ext.awaitLock(); 78 | 79 | lock.lock(); 80 | 81 | long end = System.currentTimeMillis(); 82 | 83 | lock.unlock(); 84 | 85 | String message = "expected " + wait + "ms wait, real delta: " + (end - start); 86 | assertTrue(message, end - start > wait); 87 | } 88 | 89 | @Test 90 | public void testUpgradeSharedToExclusiveLock() throws Throwable { 91 | final File file = TestFileUtils.createTempFile(""); 92 | 93 | TestFramework.runOnce(new MultithreadedTestCase() { 94 | public void thread1() throws IOException { 95 | Lock lock = manager.readLock(file); 96 | lock.lock(); 97 | assertTrue("read lock is not shared", lock.isShared()); 98 | waitForTick(2); 99 | lock.unlock(); 100 | } 101 | 102 | public void thread2() throws IOException { 103 | waitForTick(1); 104 | Lock lock = manager.writeLock(file); 105 | lock.lock(); 106 | assertTick(2); 107 | assertTrue("read lock did not upgrade to exclusive", !lock.isShared()); 108 | lock.unlock(); 109 | } 110 | }); 111 | } 112 | 113 | @Test 114 | public void testCanonicalFileLock() throws Exception { 115 | File file1 = TestFileUtils.createTempFile("testCanonicalFileLock"); 116 | File file2 = new File(file1.getParent() + File.separator + ".", file1.getName()); 117 | 118 | Lock lock1 = manager.readLock(file1); 119 | Lock lock2 = manager.readLock(file2); 120 | 121 | lock1.lock(); 122 | lock2.lock(); 123 | 124 | FileChannel channel1 = lock1.getRandomAccessFile().getChannel(); 125 | FileChannel channel2 = lock2.getRandomAccessFile().getChannel(); 126 | 127 | assertNotSame(channel1, channel2); 128 | assertSame(lock1.getLock(), lock2.getLock()); 129 | 130 | lock1.unlock(); 131 | assertNull(lock1.getRandomAccessFile()); 132 | assertFalse(channel1.isOpen()); 133 | 134 | assertTrue(lock2.getLock().isValid()); 135 | assertNotNull(lock2.getRandomAccessFile()); 136 | assertTrue(channel2.isOpen()); 137 | 138 | lock2.unlock(); 139 | assertNull(lock2.getRandomAccessFile()); 140 | assertFalse(channel2.isOpen()); 141 | } 142 | 143 | @Test 144 | public void testSafeUnlockOfNonAcquiredLock() throws IOException { 145 | File file = TestFileUtils.createTempFile(""); 146 | 147 | Lock lock = manager.readLock(file); 148 | lock.unlock(); 149 | } 150 | 151 | @Test 152 | public void testMultipleLocksSameThread() throws Throwable { 153 | final File a = TestFileUtils.createTempFile("a"); 154 | final File b = TestFileUtils.createTempFile("b"); 155 | 156 | TestFramework.runOnce(new MultithreadedTestCase() { 157 | private Lock r1; 158 | 159 | private Lock r2; 160 | 161 | private Lock w1; 162 | 163 | private Lock w2; 164 | 165 | public void thread1() throws IOException { 166 | r1 = manager.readLock(a); 167 | r2 = manager.readLock(a); 168 | w1 = manager.writeLock(b); 169 | w2 = manager.writeLock(b); 170 | try { 171 | 172 | r1.lock(); 173 | r2.lock(); 174 | w1.lock(); 175 | w2.lock(); 176 | 177 | assertSame(r1.getLock(), r2.getLock()); 178 | assertEquals(true, r1.getLock().isValid()); 179 | assertEquals(true, r2.getLock().isValid()); 180 | 181 | assertSame(w1.getLock(), w2.getLock()); 182 | assertEquals(true, w1.getLock().isValid()); 183 | assertEquals(true, w2.getLock().isValid()); 184 | 185 | r1.unlock(); 186 | assertEquals(true, r2.getLock().isValid()); 187 | r2.unlock(); 188 | w1.unlock(); 189 | assertEquals(true, w2.getLock().isValid()); 190 | w2.unlock(); 191 | } finally { 192 | if (w1 != null) { 193 | w1.unlock(); 194 | } 195 | if (w2 != null) { 196 | w2.unlock(); 197 | } 198 | if (r1 != null) { 199 | r1.unlock(); 200 | } 201 | if (r2 != null) { 202 | r2.unlock(); 203 | } 204 | } 205 | } 206 | 207 | }); 208 | } 209 | 210 | @Test 211 | public void testSameThreadMultipleLocksReadRead() throws Exception { 212 | File file = TestFileUtils.createTempFile(""); 213 | 214 | Lock lock1 = manager.readLock(file); 215 | Lock lock2 = manager.readLock(file); 216 | 217 | lock1.lock(); 218 | try { 219 | lock2.lock(); 220 | lock2.unlock(); 221 | } finally { 222 | lock1.unlock(); 223 | } 224 | } 225 | 226 | @Test 227 | public void testSameThreadMultipleLocksWriteWrite() throws Exception { 228 | File file = TestFileUtils.createTempFile(""); 229 | 230 | Lock lock1 = manager.writeLock(file); 231 | Lock lock2 = manager.writeLock(file); 232 | 233 | lock1.lock(); 234 | try { 235 | lock2.lock(); 236 | lock2.unlock(); 237 | } finally { 238 | lock1.unlock(); 239 | } 240 | } 241 | 242 | @Test 243 | public void testSameThreadMultipleLocksWriteRead() throws Exception { 244 | File file = TestFileUtils.createTempFile(""); 245 | 246 | Lock lock1 = manager.writeLock(file); 247 | Lock lock2 = manager.readLock(file); 248 | 249 | lock1.lock(); 250 | try { 251 | lock2.lock(); 252 | lock2.unlock(); 253 | } finally { 254 | lock1.unlock(); 255 | } 256 | } 257 | 258 | @Test 259 | public void testSameThreadMultipleLocksReadWrite() throws Exception { 260 | File file = TestFileUtils.createTempFile(""); 261 | 262 | Lock lock1 = manager.readLock(file); 263 | Lock lock2 = manager.writeLock(file); 264 | 265 | lock1.lock(); 266 | try { 267 | try { 268 | lock2.lock(); 269 | try { 270 | lock2.unlock(); 271 | } catch (IOException e) { 272 | // ignored 273 | } 274 | } catch (IllegalStateException e) { 275 | assertTrue(true); 276 | } 277 | } finally { 278 | lock1.unlock(); 279 | } 280 | } 281 | 282 | @Test 283 | public void testReentrantLock() throws Exception { 284 | File file = TestFileUtils.createTempFile(""); 285 | 286 | Lock lock = manager.readLock(file); 287 | lock.lock(); 288 | assertTrue(lock.isShared()); 289 | lock.lock(); 290 | assertTrue(lock.isShared()); 291 | lock.unlock(); 292 | assertTrue(lock.isShared()); 293 | assertNotNull(lock.getRandomAccessFile()); 294 | lock.unlock(); 295 | assertNull(lock.getRandomAccessFile()); 296 | } 297 | 298 | @Test 299 | public void testAcquiredLockDoesNotPreventLockedFileToBeDeleted() throws Exception { 300 | File file = TestFileUtils.createTempFile(""); 301 | 302 | Lock lock = manager.writeLock(file); 303 | lock.lock(); 304 | try { 305 | assertTrue(file.exists()); 306 | assertTrue(file.delete()); 307 | assertFalse(file.exists()); 308 | } finally { 309 | lock.unlock(); 310 | } 311 | } 312 | 313 | @Test 314 | public void testWaitingForAlreadyLockedFileToBeReleasedMustOnlyBlockCurrentThread() throws Exception { 315 | final File file1 = TestFileUtils.createTempFile("file1"); 316 | final File file2 = TestFileUtils.createTempFile("file2"); 317 | 318 | // external process locks files in opposite order than our process, i.e. file2 first 319 | ExternalProcessFileLocks external = new ExternalProcessFileLocks(file2, file1); 320 | 321 | final AtomicReference exception = new AtomicReference(); 322 | 323 | // this thread will block when attempting to lock file2 which will already be locked by the external process 324 | Thread thread = new Thread() { 325 | @Override 326 | public void run() { 327 | Lock lock2 = manager.writeLock(file2); 328 | try { 329 | lock2.lock(); 330 | lock2.unlock(); 331 | } catch (IOException e) { 332 | e.printStackTrace(); 333 | exception.set(e); 334 | } 335 | } 336 | }; 337 | 338 | Lock lock1 = manager.writeLock(file1); 339 | lock1.lock(); 340 | try { 341 | external.lockFiles(); 342 | external.awaitLock1(); 343 | 344 | thread.start(); 345 | 346 | // wait a little to allow the thread to block 347 | long start = System.currentTimeMillis(); 348 | while (System.currentTimeMillis() - start < 1000) { 349 | try { 350 | Thread.sleep(200); 351 | } catch (Exception e) { 352 | // irrelevant 353 | } 354 | } 355 | 356 | // this must not block or the inter-process deadlock is perfect 357 | lock1.unlock(); 358 | } finally { 359 | lock1.unlock(); 360 | } 361 | assertNull("inner thread got IOException: " + String.valueOf(exception.get()), exception.get()); 362 | } 363 | 364 | @Test 365 | public void testMultipleManagerInstancesShareTheSameLockTable() throws Exception { 366 | File file = TestFileUtils.createTempFile(""); 367 | 368 | DefaultFileManager manager2 = newManager(); 369 | 370 | Lock lock1 = manager.readLock(file); 371 | Lock lock2 = manager2.readLock(file); 372 | 373 | lock1.lock(); 374 | try { 375 | lock2.lock(); 376 | try { 377 | assertSame(lock1.getLock(), lock2.getLock()); 378 | } finally { 379 | lock2.unlock(); 380 | } 381 | } finally { 382 | lock1.unlock(); 383 | } 384 | } 385 | 386 | @Test 387 | public void testCopiedFilesHaveTheSameLastModifiedTime() throws Exception { 388 | File source = TestFileUtils.createTempFile("bytes"); 389 | source.setLastModified(source.lastModified() - 86400000); // -1 day 390 | File target = new File(TestFileUtils.createTempDir(), "target.txt"); 391 | DefaultFileManager manager = newManager(); 392 | manager.copy(source, target); 393 | assertEquals("We expect the length of the source and target to be equal.", source.length(), target.length()); 394 | assertEquals("We expect the last modified time of the source and target to be equal.", source.lastModified(), target.lastModified()); 395 | } 396 | } 397 | -------------------------------------------------------------------------------- /src/test/java/io/takari/filemanager/ExternalProcessFileLock.java: -------------------------------------------------------------------------------- 1 | package io.takari.filemanager; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.io.RandomAccessFile; 6 | import java.nio.channels.FileLock; 7 | 8 | /** 9 | * @author Benjamin Hanzelmann 10 | */ 11 | public class ExternalProcessFileLock { 12 | 13 | private final File file; 14 | 15 | public static void main(String[] args) throws Exception { 16 | String path = args[0]; 17 | String time = args[1]; 18 | 19 | File file = new File(path + ".aetherlock"); 20 | 21 | file.getParentFile().mkdirs(); 22 | 23 | int millis = Integer.valueOf(time); 24 | 25 | RandomAccessFile raf = new RandomAccessFile(file, "rw"); 26 | FileLock lock = raf.getChannel().lock(); 27 | 28 | File touchFile = getTouchFile(path); 29 | touchFile.createNewFile(); 30 | 31 | for (long start = System.currentTimeMillis(); System.currentTimeMillis() - start < 5 * 1000 && touchFile.exists();) { 32 | try { 33 | Thread.sleep(10); 34 | } catch (InterruptedException e) { 35 | // ignored 36 | } 37 | } 38 | 39 | long start = System.currentTimeMillis(); 40 | while (System.currentTimeMillis() - start < millis) { 41 | Thread.sleep(millis / 10 + 1); 42 | } 43 | 44 | lock.release(); 45 | raf.close(); 46 | } 47 | 48 | public ExternalProcessFileLock(File file) { 49 | this.file = file; 50 | } 51 | 52 | private static File getTouchFile(String path) { 53 | return new File(path + ".touch"); 54 | } 55 | 56 | public Process lockFile(int wait) throws InterruptedException, IOException { 57 | ForkJvm jvm = new ForkJvm(); 58 | jvm.addClassPathEntry(getClass()); 59 | jvm.setParameters(file.getAbsolutePath(), String.valueOf(wait)); 60 | Process p = jvm.run(getClass().getName()); 61 | p.getOutputStream().close(); 62 | return p; 63 | } 64 | 65 | public long awaitLock() { 66 | File touchFile = getTouchFile(file.getAbsolutePath()); 67 | 68 | for (long start = System.currentTimeMillis(); System.currentTimeMillis() - start < 10 * 1000;) { 69 | if (touchFile.exists()) { 70 | long now = System.currentTimeMillis(); 71 | touchFile.delete(); 72 | return now; 73 | } 74 | try { 75 | Thread.sleep(10); 76 | } catch (InterruptedException e) { 77 | // ignored 78 | } 79 | } 80 | 81 | throw new IllegalStateException("External lock on " + file + " wasn't aquired in time"); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/test/java/io/takari/filemanager/ExternalProcessFileLocks.java: -------------------------------------------------------------------------------- 1 | package io.takari.filemanager; 2 | 3 | /******************************************************************************* 4 | * Copyright (c) 2010-2013 Sonatype, Inc. 5 | * All rights reserved. This program and the accompanying materials 6 | * are made available under the terms of the Eclipse Public License v1.0 7 | * which accompanies this distribution, and is available at 8 | * http://www.eclipse.org/legal/epl-v10.html 9 | *******************************************************************************/ 10 | 11 | import java.io.File; 12 | import java.io.IOException; 13 | import java.io.RandomAccessFile; 14 | import java.nio.channels.FileLock; 15 | 16 | /** 17 | * Locks two files at once in a forked JVM. 18 | * 19 | * @author Benjamin Bentmann 20 | */ 21 | public class ExternalProcessFileLocks { 22 | 23 | private final File file1; 24 | 25 | private final File file2; 26 | 27 | public static void main(String[] args) throws Exception { 28 | String path1 = args[0]; 29 | String path2 = args[1]; 30 | 31 | File file1 = new File(path1 + ".aetherlock"); 32 | File file2 = new File(path2 + ".aetherlock"); 33 | 34 | file1.getParentFile().mkdirs(); 35 | file2.getParentFile().mkdirs(); 36 | 37 | // lock first file 38 | RandomAccessFile raf1 = new RandomAccessFile(file1, "rw"); 39 | FileLock lock1 = raf1.getChannel().lock(); 40 | 41 | // signal acquisition of first lock to parent process 42 | File touchFile = getTouchFile(path1); 43 | touchFile.createNewFile(); 44 | 45 | // lock second file 46 | RandomAccessFile raf2 = new RandomAccessFile(file2, "rw"); 47 | FileLock lock2 = raf2.getChannel().lock(); 48 | 49 | lock1.release(); 50 | raf1.close(); 51 | 52 | lock2.release(); 53 | raf2.close(); 54 | } 55 | 56 | public ExternalProcessFileLocks(File file1, File file2) { 57 | this.file1 = file1; 58 | this.file2 = file2; 59 | } 60 | 61 | private static File getTouchFile(String path) { 62 | return new File(path + ".touch"); 63 | } 64 | 65 | public Process lockFiles() throws InterruptedException, IOException { 66 | ForkJvm jvm = new ForkJvm(); 67 | jvm.addClassPathEntry(getClass()); 68 | jvm.setParameters(file1.getAbsolutePath(), file2.getAbsolutePath()); 69 | Process p = jvm.run(getClass().getName()); 70 | p.getOutputStream().close(); 71 | return p; 72 | } 73 | 74 | public long awaitLock1() { 75 | File touchFile = getTouchFile(file1.getAbsolutePath()); 76 | 77 | for (long start = System.currentTimeMillis(); System.currentTimeMillis() - start < 10 * 1000;) { 78 | if (touchFile.exists()) { 79 | long now = System.currentTimeMillis(); 80 | touchFile.delete(); 81 | return now; 82 | } 83 | try { 84 | Thread.sleep(10); 85 | } catch (InterruptedException e) { 86 | // ignored 87 | } 88 | } 89 | 90 | throw new IllegalStateException("External lock on " + file1 + " wasn't aquired in time"); 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/test/java/io/takari/filemanager/ForkJvm.java: -------------------------------------------------------------------------------- 1 | package io.takari.filemanager; 2 | 3 | /******************************************************************************* 4 | * Copyright (c) 2010-2013 Sonatype, Inc. 5 | * All rights reserved. This program and the accompanying materials 6 | * are made available under the terms of the Eclipse Public License v1.0 7 | * which accompanies this distribution, and is available at 8 | * http://www.eclipse.org/legal/epl-v10.html 9 | *******************************************************************************/ 10 | 11 | import java.io.BufferedReader; 12 | import java.io.File; 13 | import java.io.IOException; 14 | import java.io.InputStreamReader; 15 | import java.io.UnsupportedEncodingException; 16 | import java.net.URL; 17 | import java.net.URLDecoder; 18 | import java.util.ArrayList; 19 | import java.util.Arrays; 20 | import java.util.LinkedList; 21 | import java.util.List; 22 | 23 | /** 24 | * @author Benjamin Hanzelmann 25 | */ 26 | public class ForkJvm { 27 | 28 | private List classPathEntries = new ArrayList(4); 29 | 30 | private File workingDirectory = new File("."); 31 | 32 | private List parameters = new LinkedList(); 33 | 34 | public void addParameter(String parameter) { 35 | parameters.add(parameter); 36 | } 37 | 38 | /** 39 | * Adds the source JAR of the specified class/interface to the class path of the forked JVM. 40 | * 41 | * @param type The class/interface to add, may be null. 42 | */ 43 | public void addClassPathEntry(Class type) { 44 | addClassPathEntry(getClassSource(type)); 45 | } 46 | 47 | /** 48 | * Adds the specified path to the class path of the forked JVM. 49 | * 50 | * @param path The path to add, may be null. 51 | */ 52 | public void addClassPathEntry(String path) { 53 | if (path != null) { 54 | this.classPathEntries.add(path); 55 | } 56 | } 57 | 58 | /** 59 | * Adds the specified path to the class path of the forked JVM. 60 | * 61 | * @param path The path to add, may be null. 62 | */ 63 | public void addClassPathEntry(File path) { 64 | if (path != null) { 65 | this.classPathEntries.add(path.getAbsolutePath()); 66 | } 67 | } 68 | 69 | /** 70 | * Gets the JAR file or directory that contains the specified class. 71 | * 72 | * @param type The class/interface to find, may be null. 73 | * @return The absolute path to the class source location or null if unknown. 74 | */ 75 | private static File getClassSource(Class type) { 76 | if (type != null) { 77 | String classResource = type.getName().replace('.', '/') + ".class"; 78 | return getResourceSource(classResource, type.getClassLoader()); 79 | } 80 | return null; 81 | } 82 | 83 | /** 84 | * Gets the JAR file or directory that contains the specified resource. 85 | * 86 | * @param resource The absolute name of the resource to find, may be null. 87 | * @param loader The class loader to use for searching the resource, may be null. 88 | * @return The absolute path to the resource location or null if unknown. 89 | */ 90 | private static File getResourceSource(String resource, ClassLoader loader) { 91 | if (resource != null) { 92 | URL url; 93 | if (loader != null) { 94 | url = loader.getResource(resource); 95 | } else { 96 | url = ClassLoader.getSystemResource(resource); 97 | } 98 | return getResourceRoot(url, resource); 99 | } 100 | return null; 101 | } 102 | 103 | private static File getResourceRoot(URL url, String resource) { 104 | String str = url.getPath(); 105 | str = str.replace(resource, ""); 106 | try { 107 | str = URLDecoder.decode(str, "UTF-8"); 108 | } catch (UnsupportedEncodingException e) { 109 | throw new IllegalStateException("JVM broken", e); 110 | } 111 | return new File(str); 112 | } 113 | 114 | public Process run(String mainClass) throws IOException, InterruptedException { 115 | List cmd = new LinkedList(); 116 | cmd.add(getDefaultExecutable()); 117 | 118 | cmd.add("-cp"); 119 | StringBuilder classpath = new StringBuilder(); 120 | for (int i = 0; i < classPathEntries.size(); i++) { 121 | if (i != 0) { 122 | classpath.append(File.pathSeparator); 123 | } 124 | classpath.append(classPathEntries.get(i)); 125 | } 126 | cmd.add(classpath.toString()); 127 | 128 | cmd.add(mainClass); 129 | 130 | cmd.addAll(parameters); 131 | 132 | ProcessBuilder builder = new ProcessBuilder(cmd); 133 | builder.directory(workingDirectory); 134 | builder.redirectErrorStream(true); 135 | Process process = builder.start(); 136 | 137 | return process; 138 | 139 | } 140 | 141 | /** 142 | * Gets the absolute path to the JVM executable. 143 | * 144 | * @return The absolute path to the JVM executable. 145 | */ 146 | private static String getDefaultExecutable() { 147 | return System.getProperty("java.home") + File.separator + "bin" + File.separator + "java"; 148 | } 149 | 150 | public void setWorkingDirectory(File workingDirectory) { 151 | this.workingDirectory = workingDirectory; 152 | } 153 | 154 | public void setParameters(String... parameters) { 155 | this.parameters = Arrays.asList(parameters); 156 | } 157 | 158 | public static void flush(Process p) throws IOException { 159 | BufferedReader r = new BufferedReader(new InputStreamReader(p.getInputStream())); 160 | String line; 161 | while ((line = r.readLine()) != null) { 162 | System.out.println(line); 163 | } 164 | r.close(); 165 | } 166 | 167 | } 168 | -------------------------------------------------------------------------------- /src/test/java/io/takari/filemanager/MultipleThreadsLockManagerTest.java: -------------------------------------------------------------------------------- 1 | package io.takari.filemanager; 2 | 3 | /******************************************************************************* 4 | * Copyright (c) 2010-2013 Sonatype, Inc. 5 | * All rights reserved. This program and the accompanying materials 6 | * are made available under the terms of the Eclipse Public License v1.0 7 | * which accompanies this distribution, and is available at 8 | * http://www.eclipse.org/legal/epl-v10.html 9 | *******************************************************************************/ 10 | 11 | import io.takari.filemanager.Lock; 12 | import io.takari.filemanager.internal.DefaultFileManager; 13 | 14 | import java.io.File; 15 | import java.io.IOException; 16 | 17 | import org.junit.After; 18 | import org.junit.Before; 19 | import org.junit.Test; 20 | 21 | import edu.umd.cs.mtc.MultithreadedTestCase; 22 | import edu.umd.cs.mtc.TestFramework; 23 | 24 | @SuppressWarnings("unused") 25 | public class MultipleThreadsLockManagerTest { 26 | private DefaultFileManager manager; 27 | 28 | private File dir; 29 | 30 | @Before 31 | public void setup() throws IOException { 32 | manager = new DefaultFileManager(); 33 | dir = TestFileUtils.createTempDir(getClass().getSimpleName()); 34 | } 35 | 36 | @After 37 | public void tearDown() throws Exception { 38 | if (dir != null) { 39 | TestFileUtils.delete(dir); 40 | } 41 | manager = null; 42 | } 43 | 44 | @Test 45 | public void testLockCanonicalFile() throws Throwable { 46 | final File a = new File(dir, "a/b"); 47 | final File b = new File(dir, "a/./b"); 48 | 49 | TestFramework.runOnce(new MultithreadedTestCase() { 50 | public void thread1() throws IOException { 51 | Lock lock = manager.writeLock(a); 52 | lock.lock(); 53 | waitForTick(3); 54 | lock.unlock(); 55 | } 56 | 57 | public void thread2() throws IOException { 58 | waitForTick(1); 59 | Lock lock = manager.writeLock(b); 60 | lock.lock(); 61 | assertTick(3); 62 | lock.unlock(); 63 | } 64 | }); 65 | } 66 | 67 | @Test 68 | public void testWriteBlocksRead() throws Throwable { 69 | final File a = new File(dir, "a/b"); 70 | final File b = new File(dir, "a/b"); 71 | 72 | TestFramework.runOnce(new MultithreadedTestCase() { 73 | public void thread1() throws IOException { 74 | Lock lock = manager.writeLock(a); 75 | lock.lock(); 76 | waitForTick(2); 77 | lock.unlock(); 78 | } 79 | 80 | public void thread2() throws IOException { 81 | waitForTick(1); 82 | Lock lock = manager.readLock(b); 83 | lock.lock(); 84 | assertTick(2); 85 | lock.unlock(); 86 | } 87 | }); 88 | } 89 | 90 | @Test 91 | public void testReadDoesNotBlockRead() throws Throwable { 92 | final File a = new File(dir, "a/b"); 93 | final File b = new File(dir, "a/b"); 94 | 95 | TestFramework.runOnce(new MultithreadedTestCase() { 96 | public void thread1() throws IOException { 97 | Lock lock = manager.readLock(a); 98 | lock.lock(); 99 | waitForTick(2); 100 | lock.unlock(); 101 | } 102 | 103 | public void thread2() throws IOException { 104 | waitForTick(1); 105 | Lock lock = manager.readLock(b); 106 | lock.lock(); 107 | assertTick(1); 108 | lock.unlock(); 109 | } 110 | }); 111 | } 112 | 113 | @Test 114 | public void testReadBlocksWrite() throws Throwable { 115 | final File a = new File(dir, "a/b"); 116 | final File b = new File(dir, "a/b"); 117 | 118 | TestFramework.runOnce(new MultithreadedTestCase() { 119 | public void thread1() throws IOException { 120 | Lock lock = manager.readLock(a); 121 | lock.lock(); 122 | waitForTick(2); 123 | lock.unlock(); 124 | } 125 | 126 | public void thread2() throws IOException { 127 | waitForTick(1); 128 | Lock lock = manager.writeLock(b); 129 | lock.lock(); 130 | assertTick(2); 131 | lock.unlock(); 132 | } 133 | }); 134 | } 135 | 136 | @Test 137 | public void testWriteBlocksWrite() throws Throwable { 138 | final File a = new File(dir, "a/b"); 139 | final File b = new File(dir, "a/b"); 140 | 141 | TestFramework.runOnce(new MultithreadedTestCase() { 142 | public void thread1() throws IOException { 143 | Lock lock = manager.writeLock(a); 144 | lock.lock(); 145 | waitForTick(2); 146 | lock.unlock(); 147 | } 148 | 149 | public void thread2() throws IOException { 150 | waitForTick(1); 151 | Lock lock = manager.writeLock(b); 152 | lock.lock(); 153 | assertTick(2); 154 | lock.unlock(); 155 | } 156 | }); 157 | } 158 | 159 | @Test 160 | public void testNoPrematureLocking() throws Throwable { 161 | final File a = new File(dir, "a/b"); 162 | 163 | TestFramework.runOnce(new MultithreadedTestCase() { 164 | public void thread1() throws IOException { 165 | Lock lock = manager.readLock(a); 166 | waitForTick(2); 167 | } 168 | 169 | public void thread2() throws IOException { 170 | waitForTick(1); 171 | Lock lock = manager.writeLock(a); 172 | lock.lock(); 173 | assertTick(1); 174 | lock.unlock(); 175 | } 176 | }); 177 | } 178 | 179 | } 180 | -------------------------------------------------------------------------------- /src/test/java/io/takari/filemanager/TestFileUtils.java: -------------------------------------------------------------------------------- 1 | package io.takari.filemanager; 2 | 3 | /******************************************************************************* 4 | * Copyright (c) 2010, 2013 Sonatype, Inc. 5 | * All rights reserved. This program and the accompanying materials 6 | * are made available under the terms of the Eclipse Public License v1.0 7 | * which accompanies this distribution, and is available at 8 | * http://www.eclipse.org/legal/epl-v10.html 9 | * 10 | * Contributors: 11 | * Sonatype, Inc. - initial API and implementation 12 | *******************************************************************************/ 13 | 14 | import java.io.BufferedOutputStream; 15 | import java.io.Closeable; 16 | import java.io.File; 17 | import java.io.FileInputStream; 18 | import java.io.FileOutputStream; 19 | import java.io.IOException; 20 | import java.io.OutputStream; 21 | import java.io.RandomAccessFile; 22 | import java.util.ArrayList; 23 | import java.util.Collection; 24 | import java.util.Properties; 25 | import java.util.UUID; 26 | 27 | import org.junit.Assert; 28 | 29 | public class TestFileUtils { 30 | 31 | private static final File TMP = new File(System.getProperty("java.io.tmpdir"), "aether-" + UUID.randomUUID().toString().substring(0, 8)); 32 | 33 | static { 34 | Runtime.getRuntime().addShutdownHook(new Thread() { 35 | @Override 36 | public void run() { 37 | try { 38 | delete(TMP); 39 | } catch (IOException e) { 40 | e.printStackTrace(); 41 | } 42 | } 43 | }); 44 | } 45 | 46 | private TestFileUtils() { 47 | // hide constructor 48 | } 49 | 50 | public static void deleteTempFiles() throws IOException { 51 | delete(TMP); 52 | } 53 | 54 | public static File createTempFile(String contents) throws IOException { 55 | return createTempFile(contents.getBytes("UTF-8"), 1); 56 | } 57 | 58 | public static File createTempFile(byte[] pattern, int repeat) throws IOException { 59 | mkdirs(TMP); 60 | File tmpFile = File.createTempFile("tmpfile-", ".data", TMP); 61 | write(pattern, repeat, tmpFile); 62 | 63 | return tmpFile; 64 | } 65 | 66 | public static void write(String content, File file) throws IOException { 67 | write(content.getBytes("UTF-8"), 1, file); 68 | } 69 | 70 | public static void write(byte[] pattern, int repeat, File file) throws IOException { 71 | file.deleteOnExit(); 72 | file.getParentFile().mkdirs(); 73 | OutputStream out = null; 74 | try { 75 | out = new BufferedOutputStream(new FileOutputStream(file)); 76 | for (int i = 0; i < repeat; i++) { 77 | out.write(pattern); 78 | } 79 | } finally { 80 | close(out); 81 | } 82 | } 83 | 84 | public static long copy(File source, File target) throws IOException { 85 | long total = 0; 86 | 87 | FileInputStream fis = null; 88 | OutputStream fos = null; 89 | try { 90 | fis = new FileInputStream(source); 91 | 92 | mkdirs(target.getParentFile()); 93 | 94 | fos = new BufferedOutputStream(new FileOutputStream(target)); 95 | 96 | for (byte[] buffer = new byte[1024 * 32];;) { 97 | int bytes = fis.read(buffer); 98 | if (bytes < 0) { 99 | break; 100 | } 101 | 102 | fos.write(buffer, 0, bytes); 103 | 104 | total += bytes; 105 | } 106 | } finally { 107 | close(fis); 108 | close(fos); 109 | } 110 | 111 | return total; 112 | } 113 | 114 | private static void close(Closeable c) throws IOException { 115 | if (c != null) { 116 | try { 117 | c.close(); 118 | } catch (IOException e) { 119 | // ignore 120 | } 121 | } 122 | } 123 | 124 | public static void delete(File file) throws IOException { 125 | if (file == null) { 126 | return; 127 | } 128 | 129 | Collection undeletables = new ArrayList(); 130 | 131 | delete(file, undeletables); 132 | 133 | if (!undeletables.isEmpty()) { 134 | throw new IOException("Failed to delete " + undeletables); 135 | } 136 | } 137 | 138 | private static void delete(File file, Collection undeletables) { 139 | String[] children = file.list(); 140 | if (children != null) { 141 | for (String child : children) { 142 | delete(new File(file, child), undeletables); 143 | } 144 | } 145 | 146 | if (!del(file)) { 147 | undeletables.add(file.getAbsoluteFile()); 148 | } 149 | } 150 | 151 | private static boolean del(File file) { 152 | for (int i = 0; i < 10; i++) { 153 | if (file.delete() || !file.exists()) { 154 | return true; 155 | } 156 | } 157 | return false; 158 | } 159 | 160 | public static byte[] getContent(File file) throws IOException { 161 | RandomAccessFile in = null; 162 | try { 163 | in = new RandomAccessFile(file, "r"); 164 | byte[] actual = new byte[(int) in.length()]; 165 | in.readFully(actual); 166 | return actual; 167 | } finally { 168 | close(in); 169 | } 170 | } 171 | 172 | public static void assertContent(byte[] expected, File file) throws IOException { 173 | Assert.assertArrayEquals(expected, getContent(file)); 174 | } 175 | 176 | public static void assertContent(String expected, File file) throws IOException { 177 | byte[] content = getContent(file); 178 | String msg = new String(content, "UTF-8"); 179 | if (msg.length() > 10) { 180 | msg = msg.substring(0, 10) + "..."; 181 | } 182 | Assert.assertArrayEquals("content was '" + msg + "'\n", expected.getBytes("UTF-8"), content); 183 | } 184 | 185 | public static boolean mkdirs(File directory) { 186 | if (directory == null) { 187 | return false; 188 | } 189 | 190 | if (directory.exists()) { 191 | return false; 192 | } 193 | if (directory.mkdir()) { 194 | return true; 195 | } 196 | 197 | File canonDir = null; 198 | try { 199 | canonDir = directory.getCanonicalFile(); 200 | } catch (IOException e) { 201 | return false; 202 | } 203 | 204 | File parentDir = canonDir.getParentFile(); 205 | return (parentDir != null && (mkdirs(parentDir) || parentDir.exists()) && canonDir.mkdir()); 206 | } 207 | 208 | public static File createTempDir() throws IOException { 209 | return createTempDir(""); 210 | } 211 | 212 | public static File createTempDir(String suffix) throws IOException { 213 | mkdirs(TMP); 214 | 215 | File tmpFile = File.createTempFile("tmpdir-", suffix, TMP); 216 | 217 | delete(tmpFile); 218 | mkdirs(tmpFile); 219 | 220 | return tmpFile; 221 | } 222 | 223 | public static void read(Properties props, File file) throws IOException { 224 | FileInputStream fis = null; 225 | try { 226 | fis = new FileInputStream(file); 227 | props.load(fis); 228 | } finally { 229 | close(fis); 230 | } 231 | } 232 | 233 | public static void write(Properties props, File file) throws IOException { 234 | file.getParentFile().mkdirs(); 235 | 236 | FileOutputStream fos = null; 237 | try { 238 | fos = new FileOutputStream(file); 239 | props.store(fos, "aether-test"); 240 | } finally { 241 | close(fos); 242 | } 243 | } 244 | 245 | } 246 | -------------------------------------------------------------------------------- /src/test/projects/basic-it/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | io.takari.aether.localrepo.its 4 | update-check 5 | 0.1.0 6 | 7 | 8 | 9 | io.takari.aether.localrepo.its 10 | non-existent 11 | 1.0 12 | 13 | 14 | 15 | 16 | --------------------------------------------------------------------------------