package org.netbeans.gradle.project;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jtrim.concurrent.GenericUpdateTaskExecutor;
import org.jtrim.concurrent.TaskExecutor;
import org.jtrim.concurrent.UpdateTaskExecutor;
import org.jtrim.utils.ExceptionHelper;
import org.netbeans.gradle.project.model.NbGradleModel;
import org.netbeans.gradle.project.model.NbGradleProjectTree;
import org.netbeans.gradle.project.model.SettingsGradleDef;
import org.netbeans.gradle.project.properties.global.GlobalSettingsUtils;
import org.netbeans.gradle.project.util.NbTaskExecutors;
import org.netbeans.gradle.project.util.StringUtils;
public final class DefaultGlobalSettingsFileManager implements GlobalSettingsFileManager {
private static final Logger LOGGER = Logger.getLogger(DefaultGlobalSettingsFileManager.class.getName());
private static final TaskExecutor SETTINGS_FILE_UPDATER = NbTaskExecutors.newDefaultFifoExecutor();
private static final SecureRandom RANDOM = new SecureRandom();
private static final int STAMP_SIZE = 16 ; // bytes
private final RootProjectRegistry rootProjectRegistry;
private final UpdateTaskExecutor settingsDefPersistor;
private final Lock outstandingDefsLock;
private final Map<File, SettingsDef> outstandingDefs;
private final Locker locker;
public DefaultGlobalSettingsFileManager(RootProjectRegistry rootProjectRegistry) {
ExceptionHelper.checkNotNullArgument(rootProjectRegistry, "rootProjectRegistry");
this.rootProjectRegistry = rootProjectRegistry;
this.settingsDefPersistor = new GenericUpdateTaskExecutor(SETTINGS_FILE_UPDATER);
this.outstandingDefsLock = new ReentrantLock();
this.outstandingDefs = new HashMap<>();
this.locker = new Locker();
}
@Override
public SettingsGradleDef tryGetSettingsFile(File projectDir) {
if (NbGradleModel.isBuildSrcDirectory(projectDir)) {
return SettingsGradleDef.NO_SETTINGS;
}
Path explicitSettingsFile = rootProjectRegistry.tryGetSettingsFile(projectDir);
if (explicitSettingsFile != null) {
return new SettingsGradleDef(explicitSettingsFile, false);
}
SettingsDef result = tryGetSettingsDef(projectDir);
if (result == null) {
return null;
}
return result.settingsGradleDef;
}
private static void putAllSettingsDef(
File rootProjectDir,
NbGradleProjectTree root,
SettingsGradleDef settingsDef,
String stamp,
Map<File, SettingsDef> result) {
File projectDir = root.getProjectDir();
SettingsDef def = new SettingsDef(rootProjectDir, projectDir, settingsDef, stamp);
result.put(projectDir, def);
for (NbGradleProjectTree child: root.getChildren()) {
putAllSettingsDef(rootProjectDir, child, settingsDef, stamp, result);
}
}
private void setAllSettingsDef(NbGradleModel model, String stamp) {
NbGradleProjectTree root = model.getProjectDef().getRootProject();
SettingsGradleDef settingsDef = model.getSettingsGradleDef();
File rootProjectDir = root.getProjectDir();
outstandingDefsLock.lock();
try {
putAllSettingsDef(rootProjectDir, root, settingsDef, stamp, outstandingDefs);
} finally {
outstandingDefsLock.unlock();
}
}
private String getStamp() {
byte[] rawStamp = new byte[STAMP_SIZE];
RANDOM.nextBytes(rawStamp);
return StringUtils.byteArrayToHex(rawStamp);
}
@Override
public void updateSettingsFile(NbGradleModel model) {
ExceptionHelper.checkNotNullArgument(model, "model");
setAllSettingsDef(model, getStamp());
settingsDefPersistor.execute(new Runnable() {
@Override
public void run() {
persistSettingsDefsNow();
}
});
}
private void persistSettingsDefsNow() {
try {
persistSettingsDefsNow0();
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
private void persistSettingsDefsNow0() throws IOException {
final List<SettingsDef> toSave;
outstandingDefsLock.lock();
try {
if (outstandingDefs.isEmpty()) {
return;
}
toSave = new ArrayList<>(outstandingDefs.values());
} finally {
outstandingDefsLock.unlock();
}
getLocker().doWrite(new IoTask<Void>() {
@Override
public Void run() throws IOException {
MessageDigest hashCalculator = getNameHasher();
for (SettingsDef def: toSave) {
Path savePath = tryGetProjectSaveFile(def.projectDir, hashCalculator);
if (savePath == null) {
LOGGER.log(Level.WARNING, "Cannot save settings.gradle location for projects.");
break;
}
Path settingsGradle = def.settingsGradleDef.getSettingsGradle();
Properties output = new Properties();
output.put("projectDir", def.projectDir.toString());
output.put("rootProjectDir", def.rootProjectDir.toString());
output.put("maySearchUpwards", Boolean.toString(def.settingsGradleDef.isMaySearchUpwards()));
output.put("settingsGradle", settingsGradle != null ? settingsGradle.toString() : "");
output.put("stamp", def.stamp);
Files.createDirectories(savePath.getParent());
try (OutputStream outputStream = Files.newOutputStream(savePath)) {
output.store(outputStream, null);
}
}
return null;
}
});
outstandingDefsLock.lock();
try {
for (SettingsDef def: toSave) {
if (outstandingDefs.get(def.projectDir) == def) {
outstandingDefs.remove(def.projectDir);
}
}
} finally {
outstandingDefsLock.unlock();
}
}
private SettingsDef tryGetSettingsDef(File projectDir) {
outstandingDefsLock.lock();
try {
SettingsDef outstanding = outstandingDefs.get(projectDir);
if (outstanding != null) {
return outstanding;
}
} finally {
outstandingDefsLock.unlock();
}
SettingsDef result = tryGetStoredSettingsDef(projectDir);
outstandingDefsLock.lock();
try {
SettingsDef outstanding = outstandingDefs.get(projectDir);
if (outstanding != null) {
result = outstanding;
}
} finally {
outstandingDefsLock.unlock();
}
return result;
}
private SettingsDef tryGetStoredSettingsDef(File projectDir) {
try {
return tryGetStoredSettingsDef0(projectDir);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
private SettingsDef tryGetStoredSettingsDef0(final File projectDir) throws IOException {
return getLocker().doRead(new IoTask<SettingsDef>() {
@Override
public SettingsDef run() throws IOException {
SettingsDef result = tryGetStoredSettingsDefUnsafe(projectDir);
if (result == null) {
return null;
}
if (Objects.equals(projectDir, result.projectDir)) {
return result;
}
SettingsDef rootDef = tryGetStoredSettingsDefUnsafe(projectDir);
if (rootDef == null) {
return null;
}
return Objects.equals(result.stamp, rootDef.stamp) ? result : null;
}
});
}
private SettingsDef tryGetStoredSettingsDefUnsafe(File projectDir) {
Path settingsFile = tryGetProjectSaveFile(projectDir);
if (settingsFile == null || !Files.isRegularFile(settingsFile)) {
return null;
}
Properties settings = new Properties();
try (InputStream input = Files.newInputStream(settingsFile)) {
settings.load(input);
} catch (IOException | IllegalArgumentException ex) {
LOGGER.log(Level.INFO, "Failed to load settings from: " + settingsFile, ex);
return null;
}
String storedProjectDir = settings.getProperty("projectDir", "");
if (!storedProjectDir.isEmpty() && !projectDir.equals(new File(storedProjectDir))) {
return null;
}
String rootProjectDir = settings.getProperty("rootProjectDir", "");
String maySearchUpwards = settings.getProperty("maySearchUpwards", "");
String settingsGradle = settings.getProperty("settingsGradle", "");
String stamp = settings.getProperty("stamp", "");
try {
SettingsGradleDef settingsGradleDef = new SettingsGradleDef(
settingsGradle.isEmpty() ? null : Paths.get(settingsGradle),
Boolean.parseBoolean(maySearchUpwards));
return new SettingsDef(new File(rootProjectDir), projectDir, settingsGradleDef, stamp);
} catch (InvalidPathException ex) {
LOGGER.log(Level.INFO, "Failed to parse settings settings in: " + settingsFile, ex);
return null;
}
}
private Path tryGetProjectSaveFile(File projectDir) {
return tryGetProjectSaveFile(projectDir, getNameHasher());
}
private Path tryGetProjectSaveFile(File projectDir, MessageDigest hashCalculator) {
hashCalculator.reset();
String keyHash = StringUtils.byteArrayToHex(hashCalculator.digest(projectDir.toString().getBytes(StringUtils.UTF8)));
List<String> subPaths = new ArrayList<>();
subPaths.add("settings-gradle");
if (keyHash.length() > 2) {
subPaths.add(keyHash.substring(0, 2));
subPaths.add(keyHash.substring(2) + ".properties");
}
else {
subPaths.add("XX");
subPaths.add(keyHash + ".properties");
}
return GlobalSettingsUtils.tryGetGlobalCachePath(subPaths);
}
private Locker getLocker() {
return locker;
}
private static MessageDigest getNameHasher() {
try {
return MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException ex) {
throw new RuntimeException("Unable to load the MD5 calculator.", ex);
}
}
private static final class SettingsDef {
public final File rootProjectDir;
public final File projectDir;
public final SettingsGradleDef settingsGradleDef;
public final String stamp;
public SettingsDef(
File rootProjectDir,
File projectDir,
SettingsGradleDef settingsGradleDef,
String stamp) {
ExceptionHelper.checkNotNullArgument(rootProjectDir, "rootProjectDir");
ExceptionHelper.checkNotNullArgument(projectDir, "projectDir");
ExceptionHelper.checkNotNullArgument(settingsGradleDef, "settingsGradleDef");
ExceptionHelper.checkNotNullArgument(stamp, "stamp");
this.rootProjectDir = rootProjectDir;
this.projectDir = projectDir;
this.settingsGradleDef = settingsGradleDef;
this.stamp = stamp;
}
}
private static final class Locker {
private final Lock readLock;
private final Lock writeLock;
public Locker() {
// TODO: We should also use file lock (though it is not a big issue since
// NB cannot run concurrently with itself anyway).
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
this.readLock = readWriteLock.readLock();
this.writeLock = readWriteLock.writeLock();
}
public <R> R doRead(IoTask<? extends R> task) throws IOException {
readLock.lock();
try {
return task.run();
} finally {
readLock.unlock();
}
}
public <R> R doWrite(IoTask<? extends R> task) throws IOException {
writeLock.lock();
try {
return task.run();
} finally {
writeLock.unlock();
}
}
}
private interface IoTask<R> {
public R run() throws IOException;
}
}