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.NoSuchFileException; 035import java.nio.file.Path; 036import java.nio.file.StandardOpenOption; 037import java.util.Map; 038import java.util.Properties; 039 040import org.slf4j.Logger; 041import org.slf4j.LoggerFactory; 042 043/** 044 * Manages access to a properties file. 045 * <p> 046 * Note: the file locking in this component (that predates {@link org.eclipse.aether.SyncContext}) is present only 047 * to back off two parallel implementations that coexist in Maven (this class and {@code maven-compat} one), as in 048 * certain cases the two implementations may collide on properties files. This locking must remain in place for as long 049 * as {@code maven-compat} code exists. 050 */ 051@Singleton 052@Named 053public final class DefaultTrackingFileManager implements TrackingFileManager { 054 private static final Logger LOGGER = LoggerFactory.getLogger(DefaultTrackingFileManager.class); 055 056 @Deprecated 057 @Override 058 public Properties read(File file) { 059 return read(file.toPath()); 060 } 061 062 @Override 063 public Properties read(Path path) { 064 if (Files.isReadable(path)) { 065 synchronized (mutex(path)) { 066 try (FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.READ); 067 FileLock unused = fileLock(fileChannel, true)) { 068 Properties props = new Properties(); 069 props.load(Channels.newInputStream(fileChannel)); 070 return props; 071 } catch (NoSuchFileException e) { 072 LOGGER.debug("No such file to read {}: {}", path, e.getMessage()); 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 try { 091 Files.createDirectories(path.getParent()); 092 } catch (IOException e) { 093 LOGGER.warn("Failed to create tracking file parent '{}'", path, e); 094 throw new UncheckedIOException(e); 095 } 096 synchronized (mutex(path)) { 097 try (FileChannel fileChannel = FileChannel.open( 098 path, StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE); 099 FileLock unused = fileLock(fileChannel, false)) { 100 Properties props = new Properties(); 101 if (fileChannel.size() > 0) { 102 props.load(Channels.newInputStream(fileChannel)); 103 } 104 105 for (Map.Entry<String, String> update : updates.entrySet()) { 106 if (update.getValue() == null) { 107 props.remove(update.getKey()); 108 } else { 109 props.setProperty(update.getKey(), update.getValue()); 110 } 111 } 112 113 LOGGER.debug("Writing tracking file '{}'", path); 114 ByteArrayOutputStream stream = new ByteArrayOutputStream(1024 * 2); 115 props.store( 116 stream, 117 "NOTE: This is a Maven Resolver internal implementation file" 118 + ", its format can be changed without prior notice."); 119 fileChannel.position(0); 120 int written = fileChannel.write(ByteBuffer.wrap(stream.toByteArray())); 121 fileChannel.truncate(written); 122 return props; 123 } catch (IOException e) { 124 LOGGER.warn("Failed to write tracking file '{}'", path, e); 125 throw new UncheckedIOException(e); 126 } 127 } 128 } 129 130 @Override 131 public boolean delete(File file) { 132 return delete(file.toPath()); 133 } 134 135 @Override 136 public boolean delete(Path path) { 137 if (Files.isReadable(path)) { 138 synchronized (mutex(path)) { 139 try (FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.WRITE); 140 FileLock unused = fileLock(fileChannel, false)) { 141 Files.delete(path); 142 return true; 143 } catch (NoSuchFileException e) { 144 LOGGER.debug("No such file to delete {}: {}", path, e.getMessage()); 145 } catch (IOException e) { 146 LOGGER.warn("Failed to delete tracking file '{}'", path, e); 147 throw new UncheckedIOException(e); 148 } 149 } 150 } 151 return false; 152 } 153 154 private Object mutex(Path path) { 155 return path.toAbsolutePath().normalize().toString().intern(); 156 } 157 158 private FileLock fileLock(FileChannel channel, boolean shared) throws IOException { 159 FileLock lock = null; 160 for (int attempts = 8; attempts >= 0; attempts--) { 161 try { 162 lock = channel.lock(0, Long.MAX_VALUE, shared); 163 break; 164 } catch (OverlappingFileLockException e) { 165 if (attempts <= 0) { 166 throw new IOException(e); 167 } 168 try { 169 Thread.sleep(50L); 170 } catch (InterruptedException e1) { 171 Thread.currentThread().interrupt(); 172 } 173 } 174 } 175 if (lock == null) { 176 throw new IOException("Could not lock file"); 177 } 178 return lock; 179 } 180}