/*
* 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.notification.Notification;
import com.intellij.notification.NotificationType;
import com.intellij.notification.Notifications;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.*;
import com.intellij.openapi.util.JDOMUtil;
import com.intellij.openapi.util.io.BufferExposingByteArrayOutputStream;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.*;
import com.intellij.openapi.vfs.tracker.VirtualFileTracker;
import com.intellij.util.LineSeparator;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.util.Set;
public class FileBasedStorage extends XmlElementStorage {
private final String myFilePath;
private final File myFile;
private volatile VirtualFile myCachedVirtualFile;
private LineSeparator myLineSeparator;
public FileBasedStorage(@NotNull String filePath,
@NotNull String fileSpec,
@Nullable RoamingType roamingType,
@Nullable TrackingPathMacroSubstitutor pathMacroManager,
@NotNull String rootElementName,
@NotNull Disposable parentDisposable,
@Nullable final Listener listener,
@Nullable StreamProvider streamProvider) {
super(fileSpec, roamingType, pathMacroManager, rootElementName, streamProvider);
myFilePath = filePath;
myFile = new File(filePath);
if (listener != null) {
VirtualFileTracker virtualFileTracker = ServiceManager.getService(VirtualFileTracker.class);
if (virtualFileTracker != null) {
virtualFileTracker.addTracker(LocalFileSystem.PROTOCOL_PREFIX + myFile.getAbsolutePath().replace(File.separatorChar, '/'), new VirtualFileAdapter() {
@Override
public void fileMoved(@NotNull VirtualFileMoveEvent event) {
myCachedVirtualFile = null;
}
@Override
public void fileDeleted(@NotNull VirtualFileEvent event) {
myCachedVirtualFile = null;
}
@Override
public void fileCreated(@NotNull VirtualFileEvent event) {
myCachedVirtualFile = event.getFile();
}
@Override
public void contentsChanged(@NotNull final VirtualFileEvent event) {
listener.storageFileChanged(event, FileBasedStorage.this);
}
}, false, parentDisposable);
}
}
}
protected boolean isUseXmlProlog() {
return false;
}
protected boolean isUseLfLineSeparatorByDefault() {
return isUseXmlProlog();
}
@Override
protected XmlElementStorageSaveSession createSaveSession(@NotNull StorageData storageData) {
return new FileSaveSession(storageData);
}
public void forceSave() {
XmlElementStorageSaveSession externalizationSession = startExternalization();
if (externalizationSession != null) {
externalizationSession.forceSave();
}
}
private class FileSaveSession extends XmlElementStorageSaveSession {
protected FileSaveSession(@NotNull StorageData storageData) {
super(storageData);
}
@Override
protected void doSave(@Nullable Element element) throws IOException {
if (myLineSeparator == null) {
myLineSeparator = isUseLfLineSeparatorByDefault() ? LineSeparator.LF : LineSeparator.getSystemLineSeparator();
}
BufferExposingByteArrayOutputStream content = element == null ? null : StorageUtil.writeToBytes(element, myLineSeparator.getSeparatorString());
if (ApplicationManager.getApplication().isUnitTestMode() && StringUtil.startsWithChar(myFile.getPath(), '$')) {
throw new StateStorageException("It seems like some macros were not expanded for path: " + myFile.getPath());
}
try {
if (myStreamProvider != null && myStreamProvider.isEnabled()) {
// stream provider always use LF separator
saveForProvider(myLineSeparator == LineSeparator.LF ? content : null, element);
}
}
catch (Throwable e) {
LOG.error(e);
}
if (content == null) {
StorageUtil.deleteFile(myFile, this, getVirtualFile());
myCachedVirtualFile = null;
}
else {
VirtualFile file = getVirtualFile();
if (file == null || !file.exists()) {
FileUtil.createParentDirs(myFile);
file = null;
}
myCachedVirtualFile = StorageUtil.writeFile(myFile, this, file, content, isUseXmlProlog() ? myLineSeparator : null);
}
}
}
@Override
@NotNull
protected StorageData createStorageData() {
return new StorageData(myRootElementName);
}
@Nullable
public VirtualFile getVirtualFile() {
VirtualFile virtualFile = myCachedVirtualFile;
if (virtualFile == null) {
myCachedVirtualFile = virtualFile = LocalFileSystem.getInstance().findFileByIoFile(myFile);
}
return virtualFile;
}
@NotNull
public File getFile() {
return myFile;
}
@NotNull
public String getFilePath() {
return myFilePath;
}
@Override
@Nullable
protected Element loadLocalData() {
myBlockSavingTheContent = false;
try {
VirtualFile file = getVirtualFile();
if (file == null || file.isDirectory() || !file.isValid()) {
if (LOG.isDebugEnabled()) {
LOG.debug("Document was not loaded for " + myFileSpec + " file is " + (file == null ? "null" : "directory"));
}
return null;
}
if (file.getLength() == 0) {
return processReadException(null);
}
CharBuffer charBuffer = CharsetToolkit.UTF8_CHARSET.decode(ByteBuffer.wrap(file.contentsToByteArray()));
myLineSeparator = StorageUtil.detectLineSeparators(charBuffer, isUseLfLineSeparatorByDefault() ? null : LineSeparator.LF);
return JDOMUtil.loadDocument(charBuffer).getRootElement();
}
catch (JDOMException e) {
return processReadException(e);
}
catch (IOException e) {
return processReadException(e);
}
}
@Nullable
private Element processReadException(@Nullable Exception e) {
boolean contentTruncated = e == null;
myBlockSavingTheContent = !contentTruncated && (StorageUtil.isProjectOrModuleFile(myFileSpec) || myFileSpec.equals(StoragePathMacros.WORKSPACE_FILE));
if (!ApplicationManager.getApplication().isUnitTestMode() && !ApplicationManager.getApplication().isHeadlessEnvironment()) {
if (e != null) {
LOG.info(e);
}
new Notification(Notifications.SYSTEM_MESSAGES_GROUP_ID, "Load Settings",
"Cannot load settings from file '" +
myFile.getPath() + "': " +
(e == null ? "content truncated" : e.getMessage()) + "\n" +
(myBlockSavingTheContent ? "Please correct the file content" : "File content will be recreated"),
NotificationType.WARNING).notify(null);
}
return null;
}
@Override
public void setDefaultState(@NotNull Element element) {
element.setName(myRootElementName);
super.setDefaultState(element);
}
public void updatedFromStreamProvider(@NotNull Set<String> changedComponentNames, boolean deleted) {
if (myRoamingType == RoamingType.DISABLED) {
// storage roaming was changed to DISABLED, but settings repository has old state
return;
}
try {
Element newElement = deleted ? null : loadDataFromStreamProvider();
if (newElement == null) {
StorageUtil.deleteFile(myFile, this, myCachedVirtualFile);
// if data was loaded, mark as changed all loaded components
if (myLoadedData != null) {
changedComponentNames.addAll(myLoadedData.getComponentNames());
myLoadedData = null;
}
}
else if (myLoadedData != null) {
StorageData newStorageData = createStorageData();
loadState(newStorageData, newElement);
changedComponentNames.addAll(myLoadedData.getChangedComponentNames(newStorageData, myPathMacroSubstitutor));
myLoadedData = newStorageData;
}
}
catch (Throwable e) {
LOG.error(e);
}
}
@Override
public String toString() {
return getFilePath();
}
}