blob: 76f47db77533df97ccd9083d4ca941598a5723a1 [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.components.*;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.options.CurrentUserHolder;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.JDOMUtil;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.WriteExternalException;
import com.intellij.openapi.vfs.SafeWriteRequestor;
import com.intellij.openapi.vfs.VirtualFile;
import gnu.trove.THashMap;
import gnu.trove.THashSet;
import gnu.trove.TObjectLongHashMap;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.filter.ElementFilter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
public abstract class XmlElementStorage implements StateStorage, Disposable {
private static final Logger LOG = Logger.getInstance(XmlElementStorage.class);
private final static RoamingElementFilter DISABLED_ROAMING_ELEMENT_FILTER = new RoamingElementFilter(RoamingType.DISABLED);
private static final String ATTR_NAME = "name";
private static final String VERSION_FILE_SUFFIX = ".ver";
protected TrackingPathMacroSubstitutor myPathMacroSubstitutor;
@NotNull protected final String myRootElementName;
private Object mySession;
private StorageData myLoadedData;
protected final StreamProvider myStreamProvider;
protected final String myFileSpec;
protected boolean myBlockSavingTheContent = false;
protected int myUpToDateHash = -1;
private int myProviderUpToDateHash = -1;
private boolean mySavingDisabled = false;
private final Map<String, Object> myStorageComponentStates = new THashMap<String, Object>(); // at load we store Element, on setState Integer of hash
private final ComponentVersionProvider myLocalVersionProvider;
protected final RemoteComponentVersionProvider myRemoteVersionProvider;
private final RoamingType myRoamingType;
protected ComponentVersionListener myListener = new ComponentVersionListener(){
@Override
public void componentStateChanged(String componentName) {
myLocalVersionProvider.changeVersion(componentName, System.currentTimeMillis());
}
};
private boolean myDisposed;
protected XmlElementStorage(@NotNull String fileSpec,
@Nullable RoamingType roamingType,
@Nullable TrackingPathMacroSubstitutor pathMacroSubstitutor,
@NotNull Disposable parentDisposable,
@NotNull String rootElementName,
@Nullable StreamProvider streamProvider,
ComponentVersionProvider componentVersionProvider) {
myFileSpec = fileSpec;
myRoamingType = roamingType == null ? RoamingType.PER_USER : roamingType;
myPathMacroSubstitutor = pathMacroSubstitutor;
myRootElementName = rootElementName;
myStreamProvider = myRoamingType == RoamingType.DISABLED ? null : streamProvider;
Disposer.register(parentDisposable, this);
myLocalVersionProvider = componentVersionProvider;
myRemoteVersionProvider = streamProvider == null || !streamProvider.isVersioningRequired() ? null : new RemoteComponentVersionProvider();
}
protected boolean isDisposed() {
return myDisposed;
}
@Nullable
protected abstract Element loadLocalData();
@Nullable
public synchronized Element getState(@NotNull String componentName) {
final StorageData storageData = getStorageData(false);
final Element state = storageData.getState(componentName);
if (state != null) {
if (!myStorageComponentStates.containsKey(componentName)) {
myStorageComponentStates.put(componentName, state);
}
storageData.removeState(componentName);
}
return state;
}
@Override
public boolean hasState(final Object component, @NotNull String componentName, final Class<?> aClass, final boolean reloadData) throws StateStorageException {
return getStorageData(reloadData).hasState(componentName);
}
@Override
@Nullable
public <T> T getState(final Object component, @NotNull String componentName, Class<T> stateClass, @Nullable T mergeInto) throws StateStorageException {
return DefaultStateSerializer.deserializeState(getState(componentName), stateClass, mergeInto);
}
@NotNull
protected StorageData getStorageData() {
return getStorageData(false);
}
@NotNull
private 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()) {
boolean wasLoaded = false;
try {
wasLoaded = loadDataFromStreamProvider(result);
}
catch (Exception e) {
LOG.warn(e);
}
//noinspection deprecation
if (wasLoaded && !myStreamProvider.isVersioningRequired() && !(myStreamProvider instanceof OldStreamProviderAdapter || myStreamProvider instanceof CurrentUserHolder)) {
// we don't use local data if stream provider has one (to preserve backward compatibility, we don't use this logic for old stream providers)
return result;
}
}
Element element = loadLocalData();
if (element != null) {
loadState(result, element);
}
return result;
}
private boolean loadDataFromStreamProvider(@NotNull StorageData result) throws IOException, JDOMException {
assert myStreamProvider != null;
InputStream inputStream = myStreamProvider.loadContent(myFileSpec, myRoamingType);
if (inputStream == null) {
return false;
}
Element element = JDOMUtil.loadDocument(inputStream).getRootElement();
filterOutOfDate(element);
loadState(result, element);
return true;
}
private void loadState(@NotNull StorageData result, @NotNull Element element) {
result.load(element, myPathMacroSubstitutor, true);
}
@NotNull
protected StorageData createStorageData() {
return new StorageData(myRootElementName);
}
public void setDefaultState(final Element element) {
myLoadedData = createStorageData();
loadState(myLoadedData, element);
}
@Override
@NotNull
public ExternalizationSession startExternalization() {
ExternalizationSession session = new MyExternalizationSession(getStorageData().clone(), myListener);
mySession = session;
return session;
}
@Override
@NotNull
public SaveSession startSave(@NotNull final ExternalizationSession externalizationSession) {
LOG.assertTrue(mySession == externalizationSession);
final SaveSession saveSession = mySavingDisabled ? createNullSession() : createSaveSession((MyExternalizationSession)externalizationSession);
mySession = saveSession;
return saveSession;
}
private static SaveSession createNullSession() {
return new SaveSession(){
@Override
public void save() throws StateStorageException {
}
@Override
public Set<String> analyzeExternalChanges(@NotNull final Set<Pair<VirtualFile, StateStorage>> changedFiles) {
return Collections.emptySet();
}
@NotNull
@Override
public Collection<File> getStorageFilesToSave() throws StateStorageException {
return Collections.emptySet();
}
@NotNull
@Override
public List<File> getAllStorageFiles() {
return Collections.emptyList();
}
};
}
protected abstract MySaveSession createSaveSession(MyExternalizationSession externalizationSession);
@Override
public void finishSave(@NotNull final SaveSession saveSession) {
try {
if (mySession != saveSession) {
LOG.error("mySession=" + mySession + " saveSession=" + saveSession);
}
} finally {
mySession = null;
}
}
public void disableSaving() {
mySavingDisabled = true;
}
protected class MyExternalizationSession implements ExternalizationSession {
private final StorageData myStorageData;
private final ComponentVersionListener myListener;
public MyExternalizationSession(final StorageData storageData, ComponentVersionListener listener) {
myStorageData = storageData;
myListener = listener;
}
@Override
public void setState(@NotNull Object component, @NotNull String componentName, @NotNull Object state, @Nullable Storage storageSpec) {
assert mySession == this;
Element element;
try {
element = DefaultStateSerializer.serializeState(state, storageSpec);
}
catch (WriteExternalException e) {
LOG.debug(e);
return;
}
if (element == null || JDOMUtil.isEmpty(element)) {
return;
}
setState(componentName, element);
}
private synchronized void setState(@NotNull String componentName, @NotNull Element element) {
myStorageData.setState(componentName, element);
int hash = JDOMUtil.getTreeHash(element);
try {
Object oldElementState = myStorageComponentStates.get(componentName);
if (oldElementState instanceof Element && !JDOMUtil.areElementsEqual((Element)oldElementState, element) ||
oldElementState instanceof Integer && hash != (Integer)oldElementState) {
myListener.componentStateChanged(componentName);
}
}
finally {
myStorageComponentStates.put(componentName, hash);
}
}
}
@Nullable
protected Element getElement(@NotNull StorageData data) {
Element element = data.save();
if (element == null || JDOMUtil.isEmpty(element)) {
return null;
}
if (myPathMacroSubstitutor != null) {
try {
myPathMacroSubstitutor.collapsePaths(element);
}
finally {
myPathMacroSubstitutor.reset();
}
}
return element;
}
protected abstract class MySaveSession implements SaveSession, SafeWriteRequestor {
final StorageData myStorageData;
private Element myElementToSave;
public MySaveSession(MyExternalizationSession externalizationSession) {
myStorageData = externalizationSession.myStorageData;
}
public final boolean needsSave() {
assert mySession == this;
return _needsSave(calcHash());
}
private boolean _needsSave(int hash) {
if (myBlockSavingTheContent) {
return false;
}
if (myUpToDateHash == -1) {
if (hash != -1) {
if (!physicalContentNeedsSave()) {
myUpToDateHash = hash;
return false;
}
else {
return true;
}
}
else {
return true;
}
}
else if (hash != -1) {
if (hash == myUpToDateHash) {
return false;
}
if (!physicalContentNeedsSave()) {
myUpToDateHash = hash;
return false;
}
else {
return true;
}
}
else {
return physicalContentNeedsSave();
}
}
protected boolean physicalContentNeedsSave() {
return true;
}
protected abstract void doSave() throws StateStorageException;
protected int calcHash() {
return -1;
}
@Override
public final void save() throws StateStorageException {
assert mySession == this;
if (myBlockSavingTheContent) {
return;
}
int hash = calcHash();
try {
if (myStreamProvider != null && myStreamProvider.isEnabled() && (myProviderUpToDateHash == -1 || myProviderUpToDateHash != hash)) {
try {
saveForProvider();
}
catch (IOException e) {
LOG.warn(e);
}
finally {
myProviderUpToDateHash = hash;
}
}
}
finally {
saveLocally(hash);
}
}
private void saveLocally(int hash) {
try {
if (!(myUpToDateHash != -1 && myUpToDateHash == hash) && _needsSave(hash)) {
doSave();
}
}
finally {
myUpToDateHash = hash;
}
}
private void saveForProvider() throws IOException {
if (!myStreamProvider.isApplicable(myFileSpec, myRoamingType)) {
return;
}
Element element = getElementToSave();
if (element == null || element.getChildren().isEmpty()) {
myStreamProvider.delete(myFileSpec, myRoamingType);
return;
}
// skip the whole document if some component has disabled roaming type
// you must not store components with different roaming types in one document
// one exclusion: workspace file (you don't have choice in this case)
// for example, it is important for ICS ProjectId - we cannot keep project in another place,
// but this project id must not be shared
if (myFileSpec.equals(StoragePathMacros.WORKSPACE_FILE)) {
Element copiedElement = JDOMUtil.cloneElement(element, DISABLED_ROAMING_ELEMENT_FILTER);
if (copiedElement != null) {
doSaveForProvider(copiedElement, DISABLED_ROAMING_ELEMENT_FILTER.myRoamingType, myStreamProvider);
}
}
else {
doSaveForProvider(element, myRoamingType, myStreamProvider);
}
}
private void doSaveForProvider(@NotNull Element element, @NotNull RoamingType roamingType, @NotNull StreamProvider streamProvider) throws IOException {
StorageUtil.doSendContent(streamProvider, myFileSpec, element, roamingType, true);
if (streamProvider.isVersioningRequired()) {
TObjectLongHashMap<String> versions = loadVersions(element.getChildren(StorageData.COMPONENT));
if (!versions.isEmpty()) {
Element versionDoc = StateStorageManagerImpl.createComponentVersionsXml(versions);
StorageUtil.doSendContent(streamProvider, myFileSpec + VERSION_FILE_SUFFIX, versionDoc, roamingType, true);
}
}
}
@Nullable
protected Element getElementToSave() {
if (myElementToSave == null) {
myElementToSave = getElement(myStorageData);
}
return myElementToSave;
}
public StorageData getData() {
return myStorageData;
}
@Override
@Nullable
public Set<String> analyzeExternalChanges(@NotNull final Set<Pair<VirtualFile,StateStorage>> changedFiles) {
try {
Element element = loadLocalData();
StorageData storageData = createStorageData();
if (element == null) {
return Collections.emptySet();
}
loadState(storageData, element);
return storageData.getDifference(myStorageData, myPathMacroSubstitutor);
}
catch (StateStorageException e) {
LOG.info(e);
}
return null;
}
}
private TObjectLongHashMap<String> loadVersions(List<Element> elements) {
TObjectLongHashMap<String> result = new TObjectLongHashMap<String>();
for (Element component : elements) {
String name = component.getAttributeValue(ATTR_NAME);
if (name != null) {
long version = myLocalVersionProvider.getVersion(name);
if (version > 0) {
result.put(name, version);
}
}
}
return result;
}
@Override
public void dispose() {
myDisposed = true;
}
public void resetData(){
myLoadedData = null;
}
@Override
public void reload(@NotNull final Set<String> changedComponents) throws StateStorageException {
final StorageData storageData = loadData(false);
final StorageData oldLoadedData = myLoadedData;
if (oldLoadedData != null) {
Set<String> componentsToRetain = new THashSet<String>(oldLoadedData.myComponentStates.keySet());
componentsToRetain.addAll(changedComponents);
// add empty configuration tags for removed components
for (String componentToRetain : componentsToRetain) {
if (!storageData.myComponentStates.containsKey(componentToRetain) && myStorageComponentStates.containsKey(componentToRetain)) {
Element emptyElement = new Element("component");
LOG.info("Create empty component element for " + componentsToRetain);
emptyElement.setAttribute(StorageData.NAME, componentToRetain);
storageData.myComponentStates.put(componentToRetain, emptyElement);
}
}
storageData.myComponentStates.keySet().retainAll(componentsToRetain);
}
myLoadedData = storageData;
}
private void filterOutOfDate(@NotNull Element element) {
if (myRemoteVersionProvider == null) {
return;
}
Iterator<Element> iterator = element.getContent(new ElementFilter(StorageData.COMPONENT)).iterator();
while (iterator.hasNext()) {
String name = iterator.next().getAttributeValue(StorageData.NAME);
long remoteVersion = myRemoteVersionProvider.getVersion(name);
if (remoteVersion <= myLocalVersionProvider.getVersion(name)) {
iterator.remove();
}
else {
myLocalVersionProvider.changeVersion(name, remoteVersion);
}
}
}
@Nullable
Element logComponents() {
return mySession instanceof MySaveSession ? getElement(((MySaveSession)mySession).myStorageData) : null;
}
public void resetProviderCache() {
myProviderUpToDateHash = -1;
if (myRemoteVersionProvider != null) {
myRemoteVersionProvider.myProviderVersions = null;
}
}
private final class RemoteComponentVersionProvider implements ComponentVersionProvider {
private TObjectLongHashMap<String> myProviderVersions;
@Override
public long getVersion(String name) {
if (myProviderVersions == null) {
loadProviderVersions();
}
return myProviderVersions == null ? -1 : myProviderVersions.get(name);
}
@Override
public void changeVersion(String name, long version) {
if (myProviderVersions == null) {
loadProviderVersions();
}
if (myProviderVersions != null) {
myProviderVersions.put(name, version);
}
}
private void loadProviderVersions() {
assert myStreamProvider != null;
if (!myStreamProvider.isEnabled()) {
return;
}
myProviderVersions = new TObjectLongHashMap<String>();
for (RoamingType type : RoamingType.values()) {
try {
Document doc = StorageUtil.loadDocument(myStreamProvider.loadContent(myFileSpec + VERSION_FILE_SUFFIX, type));
if (doc != null) {
StateStorageManagerImpl.loadComponentVersions(myProviderVersions, doc);
}
}
catch (IOException e) {
LOG.debug(e);
}
}
}
}
private static class RoamingElementFilter extends ElementFilter {
final RoamingType myRoamingType;
public RoamingElementFilter(RoamingType roamingType) {
super(StorageData.COMPONENT);
myRoamingType = roamingType;
}
@Override
public boolean matches(Object obj) {
return super.matches(obj) && ComponentRoamingManager.getInstance().getRoamingType(((Element)obj).getAttributeValue(StorageData.NAME)) == myRoamingType;
}
}
}