001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.eclipse.aether.internal.impl; 020 021import javax.inject.Named; 022import javax.inject.Singleton; 023 024import java.io.ByteArrayOutputStream; 025import java.io.File; 026import java.io.IOException; 027import java.io.UncheckedIOException; 028import java.nio.ByteBuffer; 029import java.nio.channels.Channels; 030import java.nio.channels.FileChannel; 031import java.nio.channels.FileLock; 032import java.nio.channels.OverlappingFileLockException; 033import java.nio.file.Files; 034import java.nio.file.Path; 035import java.nio.file.StandardOpenOption; 036import java.util.Map; 037import java.util.Properties; 038 039import org.slf4j.Logger; 040import org.slf4j.LoggerFactory; 041 042/** 043 * Manages access to a properties file. 044 * <p> 045 * Note: the file locking in this component (that predates {@link org.eclipse.aether.SyncContext}) is present only 046 * to back off two parallel implementations that coexist in Maven (this class and {@code maven-compat} one), as in 047 * certain cases the two implementations may collide on properties files. This locking must remain in place for as long 048 * as {@code maven-compat} code exists. 049 */ 050@Singleton 051@Named 052public final class DefaultTrackingFileManager implements TrackingFileManager { 053 private static final Logger LOGGER = LoggerFactory.getLogger(DefaultTrackingFileManager.class); 054 055 @Deprecated 056 @Override 057 public Properties read(File file) { 058 return read(file.toPath()); 059 } 060 061 @Override 062 public Properties read(Path path) { 063 if (Files.isReadable(path)) { 064 synchronized (getMutex(path)) { 065 try { 066 long fileSize = Files.size(path); 067 try (FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.READ); 068 FileLock unused = fileLock(fileChannel, Math.max(1, fileSize), true)) { 069 Properties props = new Properties(); 070 props.load(Channels.newInputStream(fileChannel)); 071 return props; 072 } 073 } catch (IOException e) { 074 LOGGER.warn("Failed to read tracking file '{}'", path, e); 075 throw new UncheckedIOException(e); 076 } 077 } 078 } 079 return null; 080 } 081 082 @Deprecated 083 @Override 084 public Properties update(File file, Map<String, String> updates) { 085 return update(file.toPath(), updates); 086 } 087 088 @Override 089 public Properties update(Path path, Map<String, String> updates) { 090 Properties props = new Properties(); 091 try { 092 Files.createDirectories(path.getParent()); 093 } catch (IOException e) { 094 LOGGER.warn("Failed to create tracking file parent '{}'", path, e); 095 throw new UncheckedIOException(e); 096 } 097 098 synchronized (getMutex(path)) { 099 try { 100 long fileSize; 101 try { 102 fileSize = Files.size(path); 103 } catch (IOException e) { 104 fileSize = 0L; 105 } 106 try (FileChannel fileChannel = FileChannel.open( 107 path, StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE); 108 FileLock unused = fileLock(fileChannel, Math.max(1, fileSize), false)) { 109 if (fileSize > 0) { 110 props.load(Channels.newInputStream(fileChannel)); 111 } 112 113 for (Map.Entry<String, String> update : updates.entrySet()) { 114 if (update.getValue() == null) { 115 props.remove(update.getKey()); 116 } else { 117 props.setProperty(update.getKey(), update.getValue()); 118 } 119 } 120 121 LOGGER.debug("Writing tracking file '{}'", path); 122 ByteArrayOutputStream stream = new ByteArrayOutputStream(1024 * 2); 123 props.store( 124 stream, 125 "NOTE: This is a Maven Resolver internal implementation file" 126 + ", its format can be changed without prior notice."); 127 fileChannel.position(0); 128 int written = fileChannel.write(ByteBuffer.wrap(stream.toByteArray())); 129 fileChannel.truncate(written); 130 } 131 } catch (IOException e) { 132 LOGGER.warn("Failed to write tracking file '{}'", path, e); 133 throw new UncheckedIOException(e); 134 } 135 } 136 137 return props; 138 } 139 140 private Object getMutex(Path path) { 141 // The interned string of path is (mis)used as mutex, to exclude different threads going for same file, 142 // as JVM file locking happens on JVM not on Thread level. This is how original code did it ¯\_(ツ)_/¯ 143 /* 144 * NOTE: Locks held by one JVM must not overlap and using the canonical path is our best bet, still another 145 * piece of code might have locked the same file (unlikely though) or the canonical path fails to capture file 146 * identity sufficiently as is the case with Java 1.6 and symlinks on Windows. 147 */ 148 return path.toAbsolutePath().normalize().toString().intern(); 149 } 150 151 @SuppressWarnings({"checkstyle:magicnumber"}) 152 private FileLock fileLock(FileChannel channel, long size, boolean shared) throws IOException { 153 FileLock lock = null; 154 for (int attempts = 8; attempts >= 0; attempts--) { 155 try { 156 lock = channel.lock(0, size, shared); 157 break; 158 } catch (OverlappingFileLockException e) { 159 if (attempts <= 0) { 160 throw new IOException(e); 161 } 162 try { 163 Thread.sleep(50L); 164 } catch (InterruptedException e1) { 165 Thread.currentThread().interrupt(); 166 } 167 } 168 } 169 if (lock == null) { 170 throw new IOException("Could not lock file"); 171 } 172 return lock; 173 } 174}