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.nio.file.Path; 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 @Override 056 public Properties read(File file) { 057 Path filePath = file.toPath(); 058 if (Files.isReadable(filePath)) { 059 synchronized (getMutex(filePath)) { 060 try (FileInputStream stream = new FileInputStream(filePath.toFile()); 061 FileLock unused = fileLock(stream.getChannel(), Math.max(1, file.length()), true)) { 062 Properties props = new Properties(); 063 props.load(stream); 064 return props; 065 } catch (IOException e) { 066 LOGGER.warn("Failed to read tracking file '{}'", file, e); 067 throw new UncheckedIOException(e); 068 } 069 } 070 } 071 return null; 072 } 073 074 @Override 075 public Properties update(File file, Map<String, String> updates) { 076 Path filePath = file.toPath(); 077 Properties props = new Properties(); 078 079 try { 080 Files.createDirectories(filePath.getParent()); 081 } catch (IOException e) { 082 LOGGER.warn("Failed to create tracking file parent '{}'", file, e); 083 throw new UncheckedIOException(e); 084 } 085 086 synchronized (getMutex(filePath)) { 087 try (RandomAccessFile raf = new RandomAccessFile(filePath.toFile(), "rw"); 088 FileLock unused = fileLock(raf.getChannel(), Math.max(1, raf.length()), false)) { 089 if (raf.length() > 0) { 090 byte[] buffer = new byte[(int) raf.length()]; 091 raf.readFully(buffer); 092 props.load(new ByteArrayInputStream(buffer)); 093 } 094 095 for (Map.Entry<String, String> update : updates.entrySet()) { 096 if (update.getValue() == null) { 097 props.remove(update.getKey()); 098 } else { 099 props.setProperty(update.getKey(), update.getValue()); 100 } 101 } 102 103 LOGGER.debug("Writing tracking file '{}'", file); 104 ByteArrayOutputStream stream = new ByteArrayOutputStream(1024 * 2); 105 props.store( 106 stream, 107 "NOTE: This is a Maven Resolver internal implementation file" 108 + ", its format can be changed without prior notice."); 109 raf.seek(0L); 110 raf.write(stream.toByteArray()); 111 raf.setLength(raf.getFilePointer()); 112 } catch (IOException e) { 113 LOGGER.warn("Failed to write tracking file '{}'", file, e); 114 throw new UncheckedIOException(e); 115 } 116 } 117 118 return props; 119 } 120 121 private Object getMutex(Path file) { 122 /* 123 * NOTE: Locks held by one JVM must not overlap and using the canonical path is our best bet, still another 124 * piece of code might have locked the same file (unlikely though) or the canonical path fails to capture file 125 * identity sufficiently as is the case with Java 1.6 and symlinks on Windows. 126 */ 127 try { 128 return file.toRealPath().toString().intern(); 129 } catch (IOException e) { 130 LOGGER.warn("Failed to get real path {}", file, e); 131 return file.toAbsolutePath().toString().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}