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.ByteArrayInputStream; 025import java.io.ByteArrayOutputStream; 026import java.io.File; 027import java.io.FileInputStream; 028import java.io.IOException; 029import java.io.RandomAccessFile; 030import java.io.UncheckedIOException; 031import java.nio.channels.FileChannel; 032import java.nio.channels.FileLock; 033import java.nio.channels.OverlappingFileLockException; 034import java.nio.file.Files; 035import java.util.Map; 036import java.util.Properties; 037 038import org.slf4j.Logger; 039import org.slf4j.LoggerFactory; 040 041/** 042 * Manages access to a properties file. 043 * <p> 044 * Note: the file locking in this component (that predates {@link org.eclipse.aether.SyncContext}) is present only 045 * to back off two parallel implementations that coexist in Maven (this class and {@code maven-compat} one), as in 046 * certain cases the two implementations may collide on properties files. This locking must remain in place for as long 047 * as {@code maven-compat} code exists. 048 */ 049@Singleton 050@Named 051public final class DefaultTrackingFileManager implements TrackingFileManager { 052 private static final Logger LOGGER = LoggerFactory.getLogger(DefaultTrackingFileManager.class); 053 054 @Override 055 public Properties read(File file) { 056 if (Files.isReadable(file.toPath())) { 057 synchronized (getMutex(file)) { 058 try (FileInputStream stream = new FileInputStream(file); 059 FileLock unused = fileLock(stream.getChannel(), Math.max(1, file.length()), true)) { 060 Properties props = new Properties(); 061 props.load(stream); 062 return props; 063 } catch (IOException e) { 064 LOGGER.warn("Failed to read tracking file '{}'", file, e); 065 throw new UncheckedIOException(e); 066 } 067 } 068 } 069 return null; 070 } 071 072 @Override 073 public Properties update(File file, Map<String, String> updates) { 074 Properties props = new Properties(); 075 076 try { 077 Files.createDirectories(file.getParentFile().toPath()); 078 } catch (IOException e) { 079 LOGGER.warn("Failed to create tracking file parent '{}'", file, e); 080 throw new UncheckedIOException(e); 081 } 082 083 synchronized (getMutex(file)) { 084 try (RandomAccessFile raf = new RandomAccessFile(file, "rw"); 085 FileLock unused = fileLock(raf.getChannel(), Math.max(1, raf.length()), false)) { 086 if (raf.length() > 0) { 087 byte[] buffer = new byte[(int) raf.length()]; 088 raf.readFully(buffer); 089 props.load(new ByteArrayInputStream(buffer)); 090 } 091 092 for (Map.Entry<String, String> update : updates.entrySet()) { 093 if (update.getValue() == null) { 094 props.remove(update.getKey()); 095 } else { 096 props.setProperty(update.getKey(), update.getValue()); 097 } 098 } 099 100 LOGGER.debug("Writing tracking file '{}'", file); 101 ByteArrayOutputStream stream = new ByteArrayOutputStream(1024 * 2); 102 props.store( 103 stream, 104 "NOTE: This is a Maven Resolver internal implementation file" 105 + ", its format can be changed without prior notice."); 106 raf.seek(0L); 107 raf.write(stream.toByteArray()); 108 raf.setLength(raf.getFilePointer()); 109 } catch (IOException e) { 110 LOGGER.warn("Failed to write tracking file '{}'", file, e); 111 throw new UncheckedIOException(e); 112 } 113 } 114 115 return props; 116 } 117 118 private Object getMutex(File file) { 119 // The interned string of path is (mis)used as mutex, to exclude different threads going for same file, 120 // as JVM file locking happens on JVM not on Thread level. This is how original code did it ¯\_(ツ)_/¯ 121 /* 122 * NOTE: Locks held by one JVM must not overlap and using the canonical path is our best bet, still another 123 * piece of code might have locked the same file (unlikely though) or the canonical path fails to capture file 124 * identity sufficiently as is the case with Java 1.6 and symlinks on Windows. 125 */ 126 try { 127 return file.getCanonicalPath().intern(); 128 } catch (IOException e) { 129 LOGGER.warn("Failed to canonicalize path {}", file, e); 130 // TODO This is code smell and deprecated 131 return file.getAbsolutePath().intern(); 132 } 133 } 134 135 @SuppressWarnings({"checkstyle:magicnumber"}) 136 private FileLock fileLock(FileChannel channel, long size, boolean shared) throws IOException { 137 FileLock lock = null; 138 for (int attempts = 8; attempts >= 0; attempts--) { 139 try { 140 lock = channel.lock(0, size, shared); 141 break; 142 } catch (OverlappingFileLockException e) { 143 if (attempts <= 0) { 144 throw new IOException(e); 145 } 146 try { 147 Thread.sleep(50L); 148 } catch (InterruptedException e1) { 149 Thread.currentThread().interrupt(); 150 } 151 } 152 } 153 if (lock == null) { 154 throw new IOException("Could not lock file"); 155 } 156 return lock; 157 } 158}