/*
* 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.components.RoamingType;
import com.intellij.openapi.components.StateStorageException;
import com.intellij.openapi.components.Storage;
import com.intellij.openapi.components.TrackingPathMacroSubstitutor;
import com.intellij.openapi.components.store.StateStorageBase;
import com.intellij.openapi.util.JDOMUtil;
import com.intellij.openapi.util.WriteExternalException;
import com.intellij.openapi.util.io.BufferExposingByteArrayOutputStream;
import com.intellij.util.containers.ContainerUtil;
import gnu.trove.THashMap;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
public abstract class XmlElementStorage extends StateStorageBase<StorageData> {
@NotNull protected final String myRootElementName;
protected StorageData myLoadedData;
protected final StreamProvider myStreamProvider;
protected final String myFileSpec;
protected boolean myBlockSavingTheContent = false;
protected final RoamingType myRoamingType;
protected XmlElementStorage(@NotNull String fileSpec,
@Nullable RoamingType roamingType,
@Nullable TrackingPathMacroSubstitutor pathMacroSubstitutor,
@NotNull String rootElementName,
@Nullable StreamProvider streamProvider) {
super(pathMacroSubstitutor);
myFileSpec = fileSpec;
myRoamingType = roamingType == null ? RoamingType.PER_USER : roamingType;
myRootElementName = rootElementName;
myStreamProvider = myRoamingType == RoamingType.DISABLED ? null : streamProvider;
}
@Nullable
protected abstract Element loadLocalData();
@Nullable
@Override
protected Element getStateAndArchive(@NotNull StorageData storageData, @NotNull String componentName) {
return storageData.getStateAndArchive(componentName);
}
@Override
@NotNull
protected StorageData getStorageData(boolean reloadData) {
if (myLoadedData != null && !reloadData) {
return myLoadedData;
}
myLoadedData = loadData(true);
return myLoadedData;
}
@NotNull
protected StorageData loadData(boolean useProvidersData) {
StorageData result = createStorageData();
if (useProvidersData && myStreamProvider != null && myStreamProvider.isEnabled()) {
try {
Element element = loadDataFromStreamProvider();
if (element != null) {
loadState(result, element);
}
// we don't use local data if has stream provider
return result;
}
catch (Exception e) {
LOG.warn(e);
}
}
Element element = loadLocalData();
if (element != null) {
loadState(result, element);
}
return result;
}
@Nullable
protected final Element loadDataFromStreamProvider() throws IOException, JDOMException {
assert myStreamProvider != null;
return JDOMUtil.load(myStreamProvider.loadContent(myFileSpec, myRoamingType));
}
protected final void loadState(@NotNull StorageData result, @NotNull Element element) {
result.load(element, myPathMacroSubstitutor, true);
}
@NotNull
protected StorageData createStorageData() {
return new StorageData(myRootElementName);
}
public void setDefaultState(@NotNull Element element) {
myLoadedData = createStorageData();
loadState(myLoadedData, element);
}
@Override
@Nullable
public final XmlElementStorageSaveSession startExternalization() {
return checkIsSavingDisabled() ? null : createSaveSession(getStorageData());
}
protected abstract XmlElementStorageSaveSession createSaveSession(@NotNull StorageData storageData);
@Nullable
protected final Element getElement(@NotNull StorageData data, boolean collapsePaths, @NotNull Map<String, Element> newLiveStates) {
Element element = data.save(newLiveStates);
if (element == null || JDOMUtil.isEmpty(element)) {
return null;
}
if (collapsePaths && myPathMacroSubstitutor != null) {
try {
myPathMacroSubstitutor.collapsePaths(element);
}
finally {
myPathMacroSubstitutor.reset();
}
}
return element;
}
@Override
public void analyzeExternalChangesAndUpdateIfNeed(@NotNull Set<String> result) {
StorageData oldData = myLoadedData;
StorageData newData = getStorageData(true);
if (oldData == null) {
if (LOG.isDebugEnabled()) {
LOG.debug("analyzeExternalChangesAndUpdateIfNeed: old data null, load new for " + toString());
}
result.addAll(newData.getComponentNames());
}
else {
Set<String> changedComponentNames = oldData.getChangedComponentNames(newData, myPathMacroSubstitutor);
if (LOG.isDebugEnabled()) {
LOG.debug("analyzeExternalChangesAndUpdateIfNeed: changedComponentNames + " + changedComponentNames + " for " + toString());
}
if (!ContainerUtil.isEmpty(changedComponentNames)) {
result.addAll(changedComponentNames);
}
}
}
protected abstract class XmlElementStorageSaveSession implements SaveSession, ExternalizationSession {
private final StorageData myOriginalStorageData;
private StorageData myCopiedStorageData;
private final Map<String, Element> myNewLiveStates = new THashMap<String, Element>();
public XmlElementStorageSaveSession(@NotNull StorageData storageData) {
myOriginalStorageData = storageData;
}
@Nullable
@Override
public final SaveSession createSaveSession() {
return checkIsSavingDisabled() || myCopiedStorageData == null ? null : this;
}
@Override
public final void setState(@NotNull Object component, @NotNull String componentName, @NotNull Object state, @Nullable Storage storageSpec) {
Element element;
try {
element = DefaultStateSerializer.serializeState(state, storageSpec);
}
catch (WriteExternalException e) {
LOG.debug(e);
return;
}
catch (Throwable e) {
LOG.error("Unable to serialize " + componentName + " state", e);
return;
}
if (myCopiedStorageData == null) {
myCopiedStorageData = StorageData.setStateAndCloneIfNeed(componentName, element, myOriginalStorageData, myNewLiveStates);
}
else {
myCopiedStorageData.setState(componentName, element, myNewLiveStates);
}
}
public void forceSave() {
LOG.assertTrue(myCopiedStorageData == null);
if (myBlockSavingTheContent) {
return;
}
try {
doSave(getElement(myOriginalStorageData, isCollapsePathsOnSave(), Collections.<String, Element>emptyMap()));
}
catch (IOException e) {
throw new StateStorageException(e);
}
}
@Override
public final void save() {
if (myBlockSavingTheContent) {
return;
}
try {
doSave(getElement(myCopiedStorageData, isCollapsePathsOnSave(), myNewLiveStates));
myLoadedData = myCopiedStorageData;
}
catch (IOException e) {
throw new StateStorageException(e);
}
}
// only because default project store hack
protected boolean isCollapsePathsOnSave() {
return true;
}
protected abstract void doSave(@Nullable Element element) throws IOException;
protected void saveForProvider(@Nullable BufferExposingByteArrayOutputStream content, @Nullable Element element) throws IOException {
if (!myStreamProvider.isApplicable(myFileSpec, myRoamingType)) {
return;
}
if (element == null) {
myStreamProvider.delete(myFileSpec, myRoamingType);
}
else {
doSaveForProvider(element, myRoamingType, content);
}
}
private void doSaveForProvider(@NotNull Element element, @NotNull RoamingType roamingType, @Nullable BufferExposingByteArrayOutputStream content) throws IOException {
if (content == null) {
StorageUtil.sendContent(myStreamProvider, myFileSpec, element, roamingType);
}
else {
myStreamProvider.saveContent(myFileSpec, content.getInternalBuffer(), content.size(), myRoamingType);
}
}
}
}