blob: 912c239cffcbf0259b1fc829608b99b02e2313f8 [file] [log] [blame]
/*
* 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.components.impl.stores;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.*;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileTypes.FileTypeManager;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.WriteExternalException;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.*;
import com.intellij.openapi.vfs.tracker.VirtualFileTracker;
import com.intellij.util.SmartList;
import com.intellij.util.containers.SmartHashSet;
import com.intellij.util.messages.MessageBus;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.picocontainer.PicoContainer;
import java.io.File;
import java.io.IOException;
import java.util.*;
//todo: support missing plugins
//todo: support storage data
public class DirectoryBasedStorage implements StateStorage, Disposable {
private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.components.impl.stores.DirectoryBasedStorage");
private final TrackingPathMacroSubstitutor myPathMacroSubstitutor;
private final File myDir;
private final StateSplitter mySplitter;
private final FileTypeManager myFileTypeManager;
private Object mySession;
private DirectoryStorageData myStorageData = null;
public DirectoryBasedStorage(@Nullable TrackingPathMacroSubstitutor pathMacroSubstitutor,
@NotNull String dir,
@NotNull StateSplitter splitter,
@NotNull Disposable parentDisposable,
@NotNull PicoContainer picoContainer) {
myPathMacroSubstitutor = pathMacroSubstitutor;
myDir = new File(dir);
mySplitter = splitter;
Disposer.register(parentDisposable, this);
VirtualFileTracker virtualFileTracker = (VirtualFileTracker)picoContainer.getComponentInstanceOfType(VirtualFileTracker.class);
MessageBus messageBus = (MessageBus)picoContainer.getComponentInstanceOfType(MessageBus.class);
if (virtualFileTracker != null && messageBus != null) {
final String path = myDir.getAbsolutePath();
final String fileUrl = LocalFileSystem.PROTOCOL_PREFIX + path.replace(File.separatorChar, '/');
final Listener listener = messageBus.syncPublisher(STORAGE_TOPIC);
virtualFileTracker.addTracker(fileUrl, new VirtualFileAdapter() {
@Override
public void contentsChanged(@NotNull final VirtualFileEvent event) {
if (!StringUtil.endsWithIgnoreCase(event.getFile().getName(), ".xml")) return;
listener.storageFileChanged(event, DirectoryBasedStorage.this);
}
@Override
public void fileDeleted(@NotNull final VirtualFileEvent event) {
if (!StringUtil.endsWithIgnoreCase(event.getFile().getName(), ".xml")) return;
listener.storageFileChanged(event, DirectoryBasedStorage.this);
}
@Override
public void fileCreated(@NotNull final VirtualFileEvent event) {
if (!StringUtil.endsWithIgnoreCase(event.getFile().getName(), ".xml")) return;
listener.storageFileChanged(event, DirectoryBasedStorage.this);
}
}, false, this);
}
myFileTypeManager = FileTypeManager.getInstance();
}
@Override
@Nullable
public <T> T getState(final Object component, @NotNull final String componentName, Class<T> stateClass, @Nullable T mergeInto)
throws StateStorageException {
if (myStorageData == null) myStorageData = loadState();
if (!myStorageData.containsComponent(componentName)) {
return DefaultStateSerializer.deserializeState(new Element(StorageData.COMPONENT), stateClass, mergeInto);
}
return myStorageData.getMergedState(componentName, stateClass, mySplitter, mergeInto);
}
private DirectoryStorageData loadState() throws StateStorageException {
DirectoryStorageData storageData = new DirectoryStorageData();
storageData.loadFrom(LocalFileSystem.getInstance().findFileByIoFile(myDir), myPathMacroSubstitutor);
return storageData;
}
@Override
public boolean hasState(final Object component, @NotNull String componentName, final Class<?> aClass, final boolean reloadData) throws StateStorageException {
if (!myDir.exists()) return false;
if (reloadData) myStorageData = null;
return true;
}
@Override
@NotNull
public ExternalizationSession startExternalization() {
if (myStorageData == null) {
try {
myStorageData = loadState();
}
catch (StateStorageException e) {
LOG.error(e);
}
}
final ExternalizationSession session = new MyExternalizationSession(myStorageData.clone());
mySession = session;
return session;
}
@Override
@NotNull
public SaveSession startSave(@NotNull final ExternalizationSession externalizationSession) {
assert mySession == externalizationSession;
final MySaveSession session =
new MySaveSession(((MyExternalizationSession)externalizationSession).myStorageData, myPathMacroSubstitutor);
mySession = session;
return session;
}
@Override
public void finishSave(@NotNull final SaveSession saveSession) {
try {
LOG.assertTrue(mySession == saveSession);
} finally {
mySession = null;
}
}
@Override
public void reload(@NotNull final Set<String> changedComponents) throws StateStorageException {
myStorageData = loadState();
}
@Override
public void dispose() {
}
private class MySaveSession implements SaveSession, SafeWriteRequestor {
private final DirectoryStorageData myStorageData;
private final TrackingPathMacroSubstitutor myPathMacroSubstitutor;
private MySaveSession(final DirectoryStorageData storageData, final TrackingPathMacroSubstitutor pathMacroSubstitutor) {
myStorageData = storageData;
myPathMacroSubstitutor = pathMacroSubstitutor;
}
@Override
public void save() throws StateStorageException {
assert mySession == this;
final Set<String> currentNames = new SmartHashSet<String>();
File[] children = myDir.listFiles();
if (children != null) {
for (File child : children) {
final String fileName = child.getName();
if (!myFileTypeManager.isFileIgnored(fileName) && StringUtil.endsWithIgnoreCase(fileName, ".xml")) {
currentNames.add(fileName);
}
}
}
myStorageData.process(new DirectoryStorageData.StorageDataProcessor() {
@Override
public void process(final String componentName, final File file, final Element element) {
currentNames.remove(file.getName());
if (myPathMacroSubstitutor != null) {
myPathMacroSubstitutor.collapsePaths(element);
}
if (file.lastModified() <= myStorageData.getLastTimeStamp()) {
StorageUtil.save(file, element, MySaveSession.this, false, null);
myStorageData.updateLastTimestamp(file);
}
}
});
if (myDir.exists() && !currentNames.isEmpty()) {
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
if (myDir.exists()) {
for (String name : currentNames) {
File child = new File(myDir, name);
if (child.lastModified() > myStorageData.getLastTimeStamp()) {
// do not touch new files during VC update (which aren't read yet)
// now got an opposite problem: file is recreated if was removed by VC during update.
return;
}
final VirtualFile virtualFile = LocalFileSystem.getInstance().findFileByIoFile(child);
if (virtualFile != null) {
try {
LOG.debug("Removing configuration file: " + virtualFile.getPresentableUrl());
virtualFile.delete(MySaveSession.this);
}
catch (IOException e) {
LOG.error(e);
}
}
}
}
}
});
}
myStorageData.clear();
}
@Override
@Nullable
public Set<String> analyzeExternalChanges(@NotNull final Set<Pair<VirtualFile, StateStorage>> changedFiles) {
boolean containsSelf = false;
for (Pair<VirtualFile, StateStorage> pair : changedFiles) {
if (pair.second == DirectoryBasedStorage.this) {
VirtualFile file = pair.first;
if ("xml".equalsIgnoreCase(file.getExtension())) {
containsSelf = true;
break;
}
}
}
if (!containsSelf) return Collections.emptySet();
if (myStorageData.getComponentNames().size() == 0) {
// no state yet, so try to initialize it now
final DirectoryStorageData storageData = loadState();
return new HashSet<String>(storageData.getComponentNames());
}
return new HashSet<String>(myStorageData.getComponentNames());
}
@Override
@NotNull
public Collection<File> getStorageFilesToSave() throws StateStorageException {
assert mySession == this;
if (!myDir.exists()) return getAllStorageFiles();
assert myDir.isDirectory() : myDir.getPath();
final List<File> filesToSave = new ArrayList<File>();
final Set<String> currentChildNames = new SmartHashSet<String>();
File[] children = myDir.listFiles();
if (children != null) {
for (File child : children) {
if (!myFileTypeManager.isFileIgnored(child.getName())) {
currentChildNames.add(child.getName());
}
}
}
myStorageData.process(new DirectoryStorageData.StorageDataProcessor() {
@Override
public void process(final String componentName, final File file, final Element element) {
if (currentChildNames.contains(file.getName())) {
currentChildNames.remove(file.getName());
if (myPathMacroSubstitutor != null) {
myPathMacroSubstitutor.collapsePaths(element);
}
VirtualFile virtualFile = LocalFileSystem.getInstance().findFileByIoFile(file);
if (virtualFile == null || !StorageUtil.contentEquals(element, virtualFile)) {
filesToSave.add(file);
}
}
}
});
for (String childName : currentChildNames) {
filesToSave.add(new File(myDir, childName));
}
return filesToSave;
}
@Override
@NotNull
public List<File> getAllStorageFiles() {
return new SmartList<File>(myStorageData.getAllStorageFiles().keySet());
}
}
private class MyExternalizationSession implements ExternalizationSession {
private final DirectoryStorageData myStorageData;
private MyExternalizationSession(final DirectoryStorageData storageData) {
myStorageData = storageData;
}
@Override
public void setState(@NotNull final Object component, final String componentName, @NotNull final Object state, final Storage storageSpec) {
assert mySession == this;
setState(componentName, state, storageSpec);
}
private void setState(final String componentName, @NotNull Object state, final Storage storageSpec) {
try {
final Element element = DefaultStateSerializer.serializeState(state, storageSpec);
if (element != null) {
for (Pair<Element, String> pair : mySplitter.splitState(element)) {
Element e = pair.first;
String name = pair.second;
Element statePart = new Element(StorageData.COMPONENT);
statePart.setAttribute(StorageData.NAME, componentName);
statePart.addContent(e.detach());
myStorageData.put(componentName, new File(myDir, name), statePart, false);
}
}
}
catch (WriteExternalException e) {
throw new StateStorageException(e);
}
}
}
}