/* * 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.AccessToken; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.components.*; import com.intellij.openapi.components.store.ReadOnlyModificationException; import com.intellij.openapi.components.store.StateStorageBase; import com.intellij.openapi.project.ProjectBundle; import com.intellij.openapi.util.JDOMUtil; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.WriteExternalException; import com.intellij.openapi.util.io.BufferExposingByteArrayOutputStream; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileAdapter; import com.intellij.openapi.vfs.VirtualFileEvent; import com.intellij.openapi.vfs.tracker.VirtualFileTracker; import com.intellij.util.SystemProperties; import com.intellij.util.containers.SmartHashSet; import gnu.trove.TObjectObjectProcedure; import org.jdom.Element; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.util.Set; public class DirectoryBasedStorage extends StateStorageBase<DirectoryStorageData> { private final File myDir; private volatile VirtualFile myVirtualFile; private final StateSplitterEx mySplitter; private DirectoryStorageData myStorageData; public DirectoryBasedStorage(@Nullable TrackingPathMacroSubstitutor pathMacroSubstitutor, @NotNull String dir, @NotNull StateSplitterEx splitter, @NotNull Disposable parentDisposable, @Nullable final Listener listener) { super(pathMacroSubstitutor); myDir = new File(dir); mySplitter = splitter; VirtualFileTracker virtualFileTracker = ServiceManager.getService(VirtualFileTracker.class); if (virtualFileTracker != null && listener != null) { virtualFileTracker.addTracker(LocalFileSystem.PROTOCOL_PREFIX + myDir.getAbsolutePath().replace(File.separatorChar, '/'), new VirtualFileAdapter() { @Override public void contentsChanged(@NotNull VirtualFileEvent event) { notifyIfNeed(event); } @Override public void fileDeleted(@NotNull VirtualFileEvent event) { if (event.getFile().equals(myVirtualFile)) { myVirtualFile = null; } notifyIfNeed(event); } @Override public void fileCreated(@NotNull VirtualFileEvent event) { notifyIfNeed(event); } private void notifyIfNeed(@NotNull VirtualFileEvent event) { // storage directory will be removed if the only child was removed if (event.getFile().isDirectory() || DirectoryStorageData.isStorageFile(event.getFile())) { listener.storageFileChanged(event, DirectoryBasedStorage.this); } } }, false, parentDisposable); } } @Override public void analyzeExternalChangesAndUpdateIfNeed(@NotNull Set<String> result) { // todo reload only changed file, compute diff DirectoryStorageData oldData = myStorageData; DirectoryStorageData newData = loadState(); myStorageData = newData; if (oldData == null) { result.addAll(newData.getComponentNames()); } else { result.addAll(oldData.getComponentNames()); result.addAll(newData.getComponentNames()); } } @Nullable @Override protected Element getStateAndArchive(@NotNull DirectoryStorageData storageData, @NotNull String componentName) { return storageData.getCompositeStateAndArchive(componentName, mySplitter); } @NotNull private DirectoryStorageData loadState() { DirectoryStorageData storageData = new DirectoryStorageData(); storageData.loadFrom(getVirtualFile(), myPathMacroSubstitutor); return storageData; } @Nullable private VirtualFile getVirtualFile() { VirtualFile virtualFile = myVirtualFile; if (virtualFile == null) { myVirtualFile = virtualFile = LocalFileSystem.getInstance().findFileByIoFile(myDir); } return virtualFile; } @Override @NotNull protected DirectoryStorageData getStorageData(boolean reloadData) { if (myStorageData != null && !reloadData) { return myStorageData; } myStorageData = loadState(); return myStorageData; } @Override @Nullable public ExternalizationSession startExternalization() { return checkIsSavingDisabled() ? null : new MySaveSession(this, getStorageData()); } @NotNull public static VirtualFile createDir(@NotNull File ioDir, @NotNull Object requestor) { //noinspection ResultOfMethodCallIgnored ioDir.mkdirs(); String parentFile = ioDir.getParent(); VirtualFile parentVirtualFile = parentFile == null ? null : LocalFileSystem.getInstance().refreshAndFindFileByPath(parentFile.replace(File.separatorChar, '/')); if (parentVirtualFile == null) { throw new StateStorageException(ProjectBundle.message("project.configuration.save.file.not.found", parentFile)); } return getFile(ioDir.getName(), parentVirtualFile, requestor); } @NotNull public static VirtualFile getFile(@NotNull String fileName, @NotNull VirtualFile parentVirtualFile, @NotNull Object requestor) { VirtualFile file = parentVirtualFile.findChild(fileName); if (file != null) { return file; } AccessToken token = ApplicationManager.getApplication().acquireWriteActionLock(DirectoryBasedStorage.class); try { return parentVirtualFile.createChildData(requestor, fileName); } catch (IOException e) { throw new StateStorageException(e); } finally { token.finish(); } } private static class MySaveSession implements SaveSession, ExternalizationSession { private final DirectoryBasedStorage storage; private final DirectoryStorageData originalStorageData; private DirectoryStorageData copiedStorageData; private final Set<String> dirtyFileNames = new SmartHashSet<String>(); private final Set<String> removedFileNames = new SmartHashSet<String>(); private MySaveSession(@NotNull DirectoryBasedStorage storage, @NotNull DirectoryStorageData storageData) { this.storage = storage; originalStorageData = storageData; } @Override public void setState(@NotNull Object component, @NotNull String componentName, @NotNull Object state, Storage storageSpec) { Element compositeState; try { compositeState = DefaultStateSerializer.serializeState(state, storageSpec); } catch (WriteExternalException e) { LOG.debug(e); return; } catch (Throwable e) { LOG.error("Unable to serialize " + componentName + " state", e); return; } removedFileNames.addAll(originalStorageData.getFileNames(componentName)); if (JDOMUtil.isEmpty(compositeState)) { doSetState(componentName, null, null); } else { for (Pair<Element, String> pair : storage.mySplitter.splitState(compositeState)) { removedFileNames.remove(pair.second); doSetState(componentName, pair.second, pair.first); } if (!removedFileNames.isEmpty()) { for (String fileName : removedFileNames) { doSetState(componentName, fileName, null); } } } } private void doSetState(@NotNull String componentName, @Nullable String fileName, @Nullable Element subState) { if (copiedStorageData == null) { copiedStorageData = DirectoryStorageData.setStateAndCloneIfNeed(componentName, fileName, subState, originalStorageData); if (copiedStorageData != null && fileName != null) { dirtyFileNames.add(fileName); } } else if (copiedStorageData.setState(componentName, fileName, subState) != null && fileName != null) { dirtyFileNames.add(fileName); } } @Override @Nullable public SaveSession createSaveSession() { return storage.checkIsSavingDisabled() || copiedStorageData == null ? null : this; } @Override public void save() { VirtualFile dir = storage.getVirtualFile(); if (copiedStorageData.isEmpty()) { if (dir != null && dir.exists()) { try { StorageUtil.deleteFile(this, dir); } catch (IOException e) { throw new StateStorageException(e); } } storage.myStorageData = copiedStorageData; return; } if (dir == null || !dir.isValid()) { dir = createDir(storage.myDir, this); } if (!dirtyFileNames.isEmpty()) { saveStates(dir); } if (dir.exists() && !removedFileNames.isEmpty()) { deleteFiles(dir); } storage.myVirtualFile = dir; storage.myStorageData = copiedStorageData; } private void saveStates(@NotNull final VirtualFile dir) { final Element storeElement = new Element(StorageData.COMPONENT); for (final String componentName : copiedStorageData.getComponentNames()) { copiedStorageData.processComponent(componentName, new TObjectObjectProcedure<String, Object>() { @Override public boolean execute(String fileName, Object state) { if (!dirtyFileNames.contains(fileName)) { return true; } Element element = copiedStorageData.stateToElement(fileName, state); if (storage.myPathMacroSubstitutor != null) { storage.myPathMacroSubstitutor.collapsePaths(element); } try { storeElement.setAttribute(StorageData.NAME, componentName); storeElement.addContent(element); BufferExposingByteArrayOutputStream byteOut; VirtualFile file = getFile(fileName, dir, MySaveSession.this); if (file.exists()) { byteOut = StorageUtil.writeToBytes(storeElement, StorageUtil.loadFile(file).second); } else { byteOut = StorageUtil.writeToBytes(storeElement, SystemProperties.getLineSeparator()); } StorageUtil.writeFile(null, MySaveSession.this, file, byteOut, null); } catch (IOException e) { LOG.error(e); } finally { element.detach(); } return true; } }); } } private void deleteFiles(@NotNull VirtualFile dir) { AccessToken token = ApplicationManager.getApplication().acquireWriteActionLock(DirectoryBasedStorage.class); try { for (VirtualFile file : dir.getChildren()) { if (removedFileNames.contains(file.getName())) { deleteFile(file, this); } } } finally { token.finish(); } } } public static void deleteFile(@NotNull VirtualFile file, @NotNull Object requestor) { try { file.delete(requestor); } catch (FileNotFoundException ignored) { throw new ReadOnlyModificationException(file); } catch (IOException e) { throw new StateStorageException(e); } } }