/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.openapi.options;
import com.intellij.openapi.application.AccessToken;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ex.DecodeDefaultsUtil;
import com.intellij.openapi.components.RoamingType;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.components.impl.stores.DirectoryBasedStorage;
import com.intellij.openapi.components.impl.stores.DirectoryStorageData;
import com.intellij.openapi.components.impl.stores.StorageUtil;
import com.intellij.openapi.components.impl.stores.StreamProvider;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.extensions.AbstractExtensionPointBean;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.InvalidDataException;
import com.intellij.openapi.util.JDOMUtil;
import com.intellij.openapi.util.WriteExternalException;
import com.intellij.openapi.util.io.BufferExposingByteArrayOutputStream;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtilRt;
import com.intellij.openapi.vfs.*;
import com.intellij.openapi.vfs.tracker.VirtualFileTracker;
import com.intellij.util.SmartList;
import com.intellij.util.ThrowableConvertor;
import com.intellij.util.containers.ContainerUtilRt;
import com.intellij.util.io.URLUtil;
import com.intellij.util.text.UniqueNameGenerator;
import gnu.trove.THashSet;
import consulo.util.pointers.Named;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.Parent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.*;
public class SchemesManagerImpl<T extends Named, E extends ExternalizableScheme> extends AbstractSchemesManager<T, E> {
private static final Logger LOG = Logger.getInstance(SchemesManagerImpl.class);
private final String myFileSpec;
private final SchemeProcessor<E> myProcessor;
private final RoamingType myRoamingType;
private final StreamProvider myProvider;
private final File myIoDir;
private VirtualFile myDir;
private String mySchemeExtension = DirectoryStorageData.DEFAULT_EXT;
private boolean myUpdateExtension;
private final Set<String> myFilesToDelete = new THashSet<String>();
public SchemesManagerImpl(@NotNull String fileSpec,
@NotNull SchemeProcessor<E> processor,
@NotNull RoamingType roamingType,
@Nullable StreamProvider provider,
@NotNull File baseDir) {
myFileSpec = fileSpec;
myProcessor = processor;
myRoamingType = roamingType;
myProvider = provider;
myIoDir = baseDir;
if (processor instanceof SchemeExtensionProvider) {
mySchemeExtension = ((SchemeExtensionProvider)processor).getSchemeExtension();
myUpdateExtension = ((SchemeExtensionProvider)processor).isUpgradeNeeded();
}
VirtualFileTracker virtualFileTracker = ServiceManager.getService(VirtualFileTracker.class);
if (virtualFileTracker != null) {
final String baseDirPath = myIoDir.getAbsolutePath().replace(File.separatorChar, '/');
virtualFileTracker.addTracker(LocalFileSystem.PROTOCOL_PREFIX + baseDirPath, new VirtualFileAdapter() {
@Override
public void contentsChanged(@NotNull VirtualFileEvent event) {
if (event.getRequestor() != null || !isMy(event)) {
return;
}
E scheme = findSchemeFor(event.getFile().getName());
T oldCurrentScheme = null;
if (scheme != null) {
oldCurrentScheme = getCurrentScheme();
//noinspection unchecked
removeScheme((T)scheme);
myProcessor.onSchemeDeleted(scheme);
}
E readScheme = readSchemeFromFile(event.getFile(), true, false);
if (readScheme != null) {
myProcessor.initScheme(readScheme);
myProcessor.onSchemeAdded(readScheme);
T newCurrentScheme = getCurrentScheme();
if (oldCurrentScheme != null && newCurrentScheme == null) {
setCurrentSchemeName(readScheme.getName());
newCurrentScheme = getCurrentScheme();
}
if (oldCurrentScheme != newCurrentScheme) {
myProcessor.onCurrentSchemeChanged((E)oldCurrentScheme);
}
}
}
@Override
public void fileCreated(@NotNull VirtualFileEvent event) {
if (event.getRequestor() == null && isMy(event)) {
E readScheme = readSchemeFromFile(event.getFile(), true, false);
if (readScheme != null) {
myProcessor.initScheme(readScheme);
myProcessor.onSchemeAdded(readScheme);
}
}
}
@Override
public void fileDeleted(@NotNull VirtualFileEvent event) {
if (event.getRequestor() == null && isMy(event)) {
E scheme = findSchemeFor(event.getFile().getName());
T oldCurrentScheme = null;
if (scheme != null) {
oldCurrentScheme = getCurrentScheme();
//noinspection unchecked
removeScheme((T)scheme);
myProcessor.onSchemeDeleted(scheme);
}
T newCurrentScheme = getCurrentScheme();
if (oldCurrentScheme != null && newCurrentScheme == null) {
if (!mySchemes.isEmpty()) {
setCurrentSchemeName(mySchemes.get(0).getName());
newCurrentScheme = getCurrentScheme();
}
}
if (oldCurrentScheme != newCurrentScheme) {
myProcessor.onCurrentSchemeChanged((E)oldCurrentScheme);
}
}
}
}, false, ApplicationManager.getApplication());
}
}
@Override
public void loadBundledScheme(@NotNull String resourceName, @NotNull Object requestor, @NotNull ThrowableConvertor<Element, T, Throwable> convertor) {
try {
URL url = requestor instanceof AbstractExtensionPointBean
? (((AbstractExtensionPointBean)requestor).getLoaderForClass().getResource(resourceName))
: DecodeDefaultsUtil.getDefaults(requestor, resourceName);
if (url == null) {
// Error shouldn't occur during this operation thus we report error instead of info
LOG.error("Cannot read scheme from " + resourceName);
return;
}
addNewScheme(convertor.convert(JDOMUtil.load(URLUtil.openStream(url))), false);
}
catch (Throwable e) {
LOG.error("Cannot read scheme from " + resourceName, e);
}
}
private boolean isMy(@NotNull VirtualFileEvent event) {
return StringUtilRt.endsWithIgnoreCase(event.getFile().getNameSequence(), mySchemeExtension);
}
@Override
@NotNull
public Collection<E> loadSchemes() {
Map<String, E> result = new LinkedHashMap<String, E>();
if (myProvider != null && myProvider.isEnabled()) {
readSchemesFromProviders(result);
}
else {
File[] files = myIoDir.listFiles();
if (files != null) {
for (File file : files) {
E scheme = readSchemeFromFile(file, false, true);
if (scheme != null) {
result.put(scheme.getName(), scheme);
}
}
}
}
Collection<E> list = result.values();
for (E scheme : list) {
myProcessor.initScheme(scheme);
checkCurrentScheme(scheme);
}
return list;
}
private E findSchemeFor(@NotNull String ioFileName) {
for (T scheme : mySchemes) {
if (scheme instanceof ExternalizableScheme) {
if (ioFileName.equals(((ExternalizableScheme)scheme).getExternalInfo().getCurrentFileName() + mySchemeExtension)) {
//noinspection CastConflictsWithInstanceof,unchecked
return (E)scheme;
}
}
}
return null;
}
@Nullable
private static Element loadElementOrNull(@Nullable InputStream stream) {
try {
return JDOMUtil.load(stream);
}
catch (JDOMException e) {
LOG.warn(e);
return null;
}
catch (IOException e) {
LOG.warn(e);
return null;
}
}
private void readSchemesFromProviders(@NotNull Map<String, E> result) {
assert myProvider != null;
for (String subPath : myProvider.listSubFiles(myFileSpec, myRoamingType)) {
try {
Element element = loadElementOrNull(myProvider.loadContent(getFileFullPath(subPath), myRoamingType));
if (element == null) {
return;
}
E scheme = readScheme(element, true);
boolean fileRenamed = false;
assert scheme != null;
T existing = findSchemeByName(scheme.getName());
if (existing instanceof ExternalizableScheme) {
String currentFileName = ((ExternalizableScheme)existing).getExternalInfo().getCurrentFileName();
if (currentFileName != null && !currentFileName.equals(subPath)) {
deleteServerFile(subPath);
subPath = currentFileName;
fileRenamed = true;
}
}
String fileName = checkFileNameIsFree(subPath, scheme.getName());
if (!fileRenamed && !fileName.equals(subPath)) {
deleteServerFile(subPath);
}
loadScheme(scheme, false, fileName);
scheme.getExternalInfo().markRemote();
result.put(scheme.getName(), scheme);
}
catch (Exception e) {
LOG.info("Cannot load data from stream provider: " + e.getMessage());
}
}
}
@NotNull
private String checkFileNameIsFree(@NotNull String subPath, @NotNull String schemeName) {
for (Named scheme : mySchemes) {
if (scheme instanceof ExternalizableScheme) {
String name = ((ExternalizableScheme)scheme).getExternalInfo().getCurrentFileName();
if (name != null &&
!schemeName.equals(scheme.getName()) &&
subPath.length() == (name.length() + mySchemeExtension.length()) &&
subPath.startsWith(name) &&
subPath.endsWith(mySchemeExtension)) {
return UniqueNameGenerator.generateUniqueName(FileUtil.sanitizeName(schemeName), collectAllFileNames());
}
}
}
return subPath;
}
@NotNull
private Collection<String> collectAllFileNames() {
Set<String> result = new THashSet<String>();
for (T scheme : mySchemes) {
if (scheme instanceof ExternalizableScheme) {
ExternalInfo externalInfo = ((ExternalizableScheme)scheme).getExternalInfo();
if (externalInfo.getCurrentFileName() != null) {
result.add(externalInfo.getCurrentFileName());
}
}
}
return result;
}
private void loadScheme(@NotNull E scheme, boolean forceAdd, @NotNull CharSequence fileName) {
String fileNameWithoutExtension = createFileName(fileName);
if (!forceAdd && myFilesToDelete.contains(fileNameWithoutExtension)) {
return;
}
T existing = findSchemeByName(scheme.getName());
if (existing != null) {
if (!Comparing.equal(existing.getClass(), scheme.getClass())) {
LOG.warn("'" + scheme.getName() + "' " + existing.getClass().getSimpleName() + " replaced with " + scheme.getClass().getSimpleName());
}
mySchemes.remove(existing);
if (existing instanceof ExternalizableScheme) {
//noinspection unchecked,CastConflictsWithInstanceof
myProcessor.onSchemeDeleted((E)existing);
}
}
//noinspection unchecked
addNewScheme((T)scheme, true);
scheme.getExternalInfo().setPreviouslySavedName(scheme.getName());
scheme.getExternalInfo().setCurrentFileName(fileNameWithoutExtension);
}
private boolean canRead(@NotNull File file) {
if (file.isDirectory()) {
return false;
}
// migrate from custom extension to default
if (myUpdateExtension && StringUtilRt.endsWithIgnoreCase(file.getName(), mySchemeExtension)) {
return true;
}
else {
return StringUtilRt.endsWithIgnoreCase(file.getName(), DirectoryStorageData.DEFAULT_EXT);
}
}
@Nullable
private E readSchemeFromFile(@NotNull VirtualFile file, boolean forceAdd, boolean duringLoad) {
return readSchemeFromFile(VfsUtil.virtualToIoFile(file), forceAdd, duringLoad);
}
@Nullable
private E readSchemeFromFile(@NotNull final File file, boolean forceAdd, boolean duringLoad) {
if (!canRead(file)) {
return null;
}
try {
Element element;
try {
element = JDOMUtil.load(file);
}
catch (JDOMException e) {
try {
FileUtil.copy(file, new File(myIoDir, file.getName() + ".copy"));
}
catch (IOException e1) {
LOG.error(e1);
}
LOG.error("Error reading file " + file.getPath() + ": " + e.getMessage());
return null;
}
E scheme = readScheme(element, duringLoad);
if (scheme != null) {
loadScheme(scheme, forceAdd, file.getName());
}
return scheme;
}
catch (final Exception e) {
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
String msg = "Cannot read scheme " + file.getName() + " from '" + myFileSpec + "': " + e.getMessage();
LOG.info(msg, e);
Messages.showErrorDialog(msg, "Load Settings");
}
}
);
return null;
}
}
@Nullable
private E readScheme(@NotNull Element element, boolean duringLoad) throws InvalidDataException, IOException, JDOMException {
E scheme;
if (myProcessor instanceof BaseSchemeProcessor) {
scheme = ((BaseSchemeProcessor<E>)myProcessor).readScheme(element, duringLoad);
}
else {
//noinspection deprecation
scheme = myProcessor.readScheme(new Document((Element)element.detach()));
}
if (scheme != null) {
scheme.getExternalInfo().setHash(JDOMUtil.getTreeHash(element, true));
}
return scheme;
}
@NotNull
private String createFileName(@NotNull CharSequence fileName) {
if (StringUtilRt.endsWithIgnoreCase(fileName, mySchemeExtension)) {
fileName = fileName.subSequence(0, fileName.length() - mySchemeExtension.length());
}
else if (StringUtilRt.endsWithIgnoreCase(fileName, DirectoryStorageData.DEFAULT_EXT)) {
fileName = fileName.subSequence(0, fileName.length() - DirectoryStorageData.DEFAULT_EXT.length());
}
return fileName.toString();
}
public void updateConfigFilesFromStreamProviders() {
// todo
}
private String getFileFullPath(@NotNull String subPath) {
return myFileSpec + '/' + subPath;
}
@Override
public void save() {
boolean hasSchemes = false;
UniqueNameGenerator nameGenerator = new UniqueNameGenerator();
List<E> schemesToSave = new SmartList<E>();
for (T scheme : mySchemes) {
if (scheme instanceof ExternalizableScheme) {
//noinspection CastConflictsWithInstanceof,unchecked
E eScheme = (E)scheme;
BaseSchemeProcessor.State state;
if (myProcessor instanceof BaseSchemeProcessor) {
state = ((BaseSchemeProcessor<E>)myProcessor).getState(eScheme);
}
else {
//noinspection deprecation
state = myProcessor.shouldBeSaved(eScheme) ? BaseSchemeProcessor.State.POSSIBLY_CHANGED : BaseSchemeProcessor.State.NON_PERSISTENT;
}
if (state == BaseSchemeProcessor.State.NON_PERSISTENT) {
continue;
}
hasSchemes = true;
if (state != BaseSchemeProcessor.State.UNCHANGED) {
schemesToSave.add(eScheme);
}
String fileName = eScheme.getExternalInfo().getCurrentFileName();
if (fileName != null && !isRenamed(eScheme)) {
nameGenerator.addExistingName(fileName);
}
}
}
if (!hasSchemes) {
myFilesToDelete.clear();
if (myIoDir.exists()) {
FileUtil.delete(myIoDir);
}
return;
}
for (final E scheme : schemesToSave) {
try {
saveScheme(scheme, nameGenerator);
}
catch (final Exception e) {
Application app = ApplicationManager.getApplication();
if (app.isUnitTestMode() || app.isCommandLine()) {
LOG.error("Cannot write scheme " + scheme.getName() + " in '" + myFileSpec + "': " + e.getLocalizedMessage(), e);
}
else {
app.invokeLater(new Runnable() {
@Override
public void run() {
Messages.showErrorDialog("Cannot save scheme '" + scheme.getName() + ": " + e.getMessage(), "Save Settings");
}
});
}
}
}
deleteFiles();
}
private void saveScheme(@NotNull E scheme, @NotNull UniqueNameGenerator nameGenerator) throws WriteExternalException, IOException {
ExternalInfo externalInfo = scheme.getExternalInfo();
String currentFileNameWithoutExtension = externalInfo.getCurrentFileName();
Parent parent = myProcessor.writeScheme(scheme);
Element element = parent == null || parent instanceof Element ? (Element)parent : ((Document)parent).detachRootElement();
if (JDOMUtil.isEmpty(element)) {
ContainerUtilRt.addIfNotNull(myFilesToDelete, currentFileNameWithoutExtension);
return;
}
String fileNameWithoutExtension = currentFileNameWithoutExtension;
if (fileNameWithoutExtension == null || isRenamed(scheme)) {
fileNameWithoutExtension = nameGenerator.generateUniqueName(FileUtil.sanitizeName(scheme.getName()));
}
String fileName = fileNameWithoutExtension + mySchemeExtension;
int newHash = JDOMUtil.getTreeHash(element, true);
if (currentFileNameWithoutExtension == fileNameWithoutExtension && newHash == externalInfo.getHash()) {
return;
}
// file will be overwritten, so, we don't need to delete it
myFilesToDelete.remove(fileNameWithoutExtension);
// stream provider always use LF separator
final BufferExposingByteArrayOutputStream byteOut = StorageUtil.writeToBytes(element, "\n");
// if another new scheme uses old name of this scheme, so, we must not delete it (as part of rename operation)
boolean renamed = currentFileNameWithoutExtension != null && fileNameWithoutExtension != currentFileNameWithoutExtension && nameGenerator.value(currentFileNameWithoutExtension);
if (!externalInfo.isRemote()) {
VirtualFile file = null;
if (renamed) {
file = myDir.findChild(currentFileNameWithoutExtension + mySchemeExtension);
if (file != null) {
AccessToken token = ApplicationManager.getApplication().acquireWriteActionLock(SchemesManagerImpl.class);
try {
file.rename(this, fileName);
}
finally {
token.finish();
}
}
}
if (file == null) {
if (myDir == null || !myDir.isValid()) {
myDir = DirectoryBasedStorage.createDir(myIoDir, this);
}
file = DirectoryBasedStorage.getFile(fileName, myDir, this);
}
AccessToken token = ApplicationManager.getApplication().acquireWriteActionLock(SchemesManagerImpl.class);
try {
OutputStream out = file.getOutputStream(this);
try {
byteOut.writeTo(out);
}
finally {
out.close();
}
}
finally {
token.finish();
}
}
else if (renamed) {
myFilesToDelete.add(currentFileNameWithoutExtension);
}
externalInfo.setHash(newHash);
externalInfo.setPreviouslySavedName(scheme.getName());
externalInfo.setCurrentFileName(createFileName(fileName));
if (myProvider != null && myProvider.isEnabled()) {
String fileSpec = getFileFullPath(fileName);
if (myProvider.isApplicable(fileSpec, myRoamingType)) {
myProvider.saveContent(fileSpec, byteOut.getInternalBuffer(), byteOut.size(), myRoamingType);
}
}
}
private static boolean isRenamed(@NotNull ExternalizableScheme scheme) {
return !scheme.getName().equals(scheme.getExternalInfo().getPreviouslySavedName());
}
private void deleteFiles() {
if (myFilesToDelete.isEmpty()) {
return;
}
if (myProvider != null && myProvider.isEnabled()) {
for (String nameWithoutExtension : myFilesToDelete) {
deleteServerFile(nameWithoutExtension + mySchemeExtension);
if (!DirectoryStorageData.DEFAULT_EXT.equals(mySchemeExtension)) {
deleteServerFile(nameWithoutExtension + DirectoryStorageData.DEFAULT_EXT);
}
}
}
VirtualFile dir = getVirtualDir();
if (dir != null) {
AccessToken token = ApplicationManager.getApplication().acquireWriteActionLock(SchemesManagerImpl.class);
try {
for (VirtualFile file : dir.getChildren()) {
if (myFilesToDelete.contains(file.getNameWithoutExtension())) {
DirectoryBasedStorage.deleteFile(file, this);
}
}
myFilesToDelete.clear();
}
finally {
token.finish();
}
}
}
@Nullable
private VirtualFile getVirtualDir() {
VirtualFile virtualFile = myDir;
if (virtualFile == null) {
myDir = virtualFile = LocalFileSystem.getInstance().findFileByIoFile(myIoDir);
}
return virtualFile;
}
@Override
public File getRootDirectory() {
return myIoDir;
}
private void deleteServerFile(@NotNull String path) {
if (myProvider != null && myProvider.isEnabled()) {
StorageUtil.delete(myProvider, getFileFullPath(path), myRoamingType);
}
}
@Override
protected void schemeDeleted(@NotNull Named scheme) {
super.schemeDeleted(scheme);
if (scheme instanceof ExternalizableScheme) {
ContainerUtilRt.addIfNotNull(myFilesToDelete, ((ExternalizableScheme)scheme).getExternalInfo().getCurrentFileName());
}
}
@Override
protected void schemeAdded(@NotNull T scheme) {
if (!(scheme instanceof ExternalizableScheme)) {
return;
}
ExternalInfo externalInfo = ((ExternalizableScheme)scheme).getExternalInfo();
String fileName = externalInfo.getCurrentFileName();
if (fileName != null) {
myFilesToDelete.remove(fileName);
}
if (myProvider != null && myProvider.isEnabled()) {
// do not save locally
externalInfo.markRemote();
}
}
}