/*
* Copyright 2000-2015 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.application.*;
import com.intellij.openapi.application.ex.DecodeDefaultsUtil;
import com.intellij.openapi.components.*;
import com.intellij.openapi.components.StateStorage.SaveSession;
import com.intellij.openapi.components.impl.stores.StateStorageManager.ExternalizationSession;
import com.intellij.openapi.components.store.ReadOnlyModificationException;
import com.intellij.openapi.components.store.StateStorageBase;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectBundle;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.JDOMUtil;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.ArrayUtilRt;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.SmartHashSet;
import com.intellij.util.messages.MessageBus;
import com.intellij.util.xmlb.JDOMXIncluder;
import consulo.components.impl.stores.StateComponentInfo;
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.net.URL;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
public abstract class ComponentStoreImpl implements IComponentStore.Reloadable {
private static final Logger LOG = Logger.getInstance(ComponentStoreImpl.class);
private final Map<String, StateComponentInfo<?>> myComponents = Collections.synchronizedMap(new THashMap<>());
private final List<SettingsSavingComponent> mySettingsSavingComponents = new CopyOnWriteArrayList<>();
@Override
public void initComponent(@NotNull Object component) {
if (component instanceof SettingsSavingComponent) {
mySettingsSavingComponents.add((SettingsSavingComponent)component);
}
StateComponentInfo<?> componentInfo = StateComponentInfo.of(component, getProject());
if (componentInfo == null) {
return;
}
AccessToken token = ReadAction.start();
try {
initComponent(componentInfo, null, false);
}
catch (StateStorageException | ProcessCanceledException e) {
throw e;
}
catch (Exception e) {
LOG.error(e);
}
finally {
token.finish();
}
}
@Override
public final void save(@NotNull List<Pair<StateStorage.SaveSession, VirtualFile>> readonlyFiles) {
ExternalizationSession externalizationSession = myComponents.isEmpty() ? null : getStateStorageManager().startExternalization();
if (externalizationSession != null) {
String[] names = ArrayUtilRt.toStringArray(myComponents.keySet());
Arrays.sort(names);
for (String name : names) {
StateComponentInfo<?> componentInfo = myComponents.get(name);
commitComponent(componentInfo, externalizationSession);
}
}
for (SettingsSavingComponent settingsSavingComponent : mySettingsSavingComponents) {
try {
settingsSavingComponent.save();
}
catch (Throwable e) {
LOG.error(e);
}
}
doSave(externalizationSession == null ? null : externalizationSession.createSaveSessions(), readonlyFiles);
}
protected void doSave(@Nullable List<SaveSession> saveSessions, @NotNull List<Pair<SaveSession, VirtualFile>> readonlyFiles) {
if (saveSessions != null) {
for (SaveSession session : saveSessions) {
executeSave(session, readonlyFiles);
}
}
}
protected static void executeSave(@NotNull SaveSession session, @NotNull List<Pair<SaveSession, VirtualFile>> readonlyFiles) {
try {
session.save();
}
catch (ReadOnlyModificationException e) {
readonlyFiles.add(Pair.create(session, e.getFile()));
}
}
private <T> void commitComponent(@NotNull StateComponentInfo<T> componentInfo, @NotNull ExternalizationSession session) {
PersistentStateComponent<T> component = componentInfo.getComponent();
T state = component.getState();
if (state != null) {
Storage[] storageSpecs = getComponentStorageSpecs(component, componentInfo.getState(), StateStorageOperation.WRITE);
session.setState(storageSpecs, component, componentInfo.getName(), state);
}
}
private void doAddComponent(@NotNull String componentName, @NotNull StateComponentInfo<?> stateComponentInfo) {
StateComponentInfo<?> existing = myComponents.get(componentName);
if (existing != null && !existing.equals(stateComponentInfo)) {
LOG.error("Conflicting component name '" + componentName + "': " + existing.getComponent().getClass() + " and " + stateComponentInfo.getComponent()
.getClass());
}
myComponents.put(componentName, stateComponentInfo);
}
@Nullable
protected Project getProject() {
return null;
}
private void validateUnusedMacros(@Nullable final String componentName, final boolean service) {
final Project project = getProject();
if (project == null) return;
if (!ApplicationManager.getApplication().isHeadlessEnvironment() && !ApplicationManager.getApplication().isUnitTestMode()) {
if (service && componentName != null && project.isInitialized()) {
final TrackingPathMacroSubstitutor substitutor = getStateStorageManager().getMacroSubstitutor();
if (substitutor != null) {
StorageUtil.notifyUnknownMacros(substitutor, project, componentName);
}
}
}
}
private <T> String initComponent(@NotNull StateComponentInfo<T> componentInfo,
@Nullable Collection<? extends StateStorage> changedStorages,
boolean reloadData) {
PersistentStateComponent<T> component = componentInfo.getComponent();
State stateSpec = componentInfo.getState();
String name = stateSpec.name();
if (changedStorages == null || !reloadData) {
doAddComponent(name, componentInfo);
}
if (optimizeTestLoading()) {
return name;
}
Class<T> stateClass = ComponentSerializationUtil.getStateClass(component.getClass());
T state = getDefaultState(component, name, stateClass);
Storage[] storageSpecs = getComponentStorageSpecs(component, stateSpec, StateStorageOperation.READ);
for (Storage storageSpec : storageSpecs) {
StateStorage stateStorage = getStateStorageManager().getStateStorage(storageSpec);
if (stateStorage != null &&
(stateStorage.hasState(component, name, stateClass, reloadData) || (changedStorages != null && changedStorages.contains(stateStorage)))) {
state = stateStorage.getState(component, name, stateClass, state);
break;
}
}
if (state != null) {
component.loadState(state);
}
validateUnusedMacros(name, true);
return name;
}
@Nullable
protected abstract PathMacroManager getPathMacroManagerForDefaults();
@Nullable
protected <T> T getDefaultState(@NotNull Object component, @NotNull String componentName, @NotNull final Class<T> stateClass) {
URL url = DecodeDefaultsUtil.getDefaults(component, componentName);
if (url == null) {
return null;
}
try {
Element documentElement = JDOMXIncluder.resolve(JDOMUtil.loadDocument(url), url.toExternalForm()).detachRootElement();
PathMacroManager pathMacroManager = getPathMacroManagerForDefaults();
if (pathMacroManager != null) {
pathMacroManager.expandPaths(documentElement);
}
return DefaultStateSerializer.deserializeState(documentElement, stateClass, null);
}
catch (IOException | JDOMException e) {
throw new StateStorageException("Error loading state from " + url, e);
}
}
@NotNull
protected <T> Storage[] getComponentStorageSpecs(@NotNull PersistentStateComponent<T> persistentStateComponent,
@NotNull State stateSpec,
@NotNull StateStorageOperation operation) {
Storage[] storages = stateSpec.storages();
if (storages.length == 1) {
return storages;
}
assert storages.length > 0;
int actualStorageCount = 0;
for (Storage storage : storages) {
if (!storage.deprecated()) {
actualStorageCount++;
}
}
if (actualStorageCount > 1) {
LOG.error("State chooser not specified for: " + persistentStateComponent.getClass());
}
if (!storages[0].deprecated()) {
boolean othersAreDeprecated = true;
for (int i = 1; i < storages.length; i++) {
if (!storages[i].deprecated()) {
othersAreDeprecated = false;
break;
}
}
if (othersAreDeprecated) {
return storages;
}
}
Storage[] sorted = Arrays.copyOf(storages, storages.length);
Arrays.sort(sorted, (o1, o2) -> {
int w1 = o1.deprecated() ? 1 : 0;
int w2 = o2.deprecated() ? 1 : 0;
return w1 - w2;
});
return sorted;
}
protected boolean optimizeTestLoading() {
return false;
}
@Override
public boolean isReloadPossible(@NotNull final Set<String> componentNames) {
for (String componentName : componentNames) {
final StateComponentInfo<?> component = myComponents.get(componentName);
if (!component.getState().reloadable()) {
return false;
}
}
return true;
}
@Override
@NotNull
public final Collection<String> getNotReloadableComponents(@NotNull Collection<String> componentNames) {
Set<String> notReloadableComponents = null;
for (String componentName : componentNames) {
StateComponentInfo<?> component = myComponents.get(componentName);
if (!component.getState().reloadable()) {
if (notReloadableComponents == null) {
notReloadableComponents = new LinkedHashSet<>();
}
notReloadableComponents.add(componentName);
}
}
return notReloadableComponents == null ? Collections.<String>emptySet() : notReloadableComponents;
}
@Override
public void reinitComponents(@NotNull Set<String> componentNames, boolean reloadData) {
reinitComponents(componentNames, Collections.<String>emptySet(), Collections.<StateStorage>emptySet());
}
protected boolean reinitComponent(@NotNull String componentName, @NotNull Collection<? extends StateStorage> changedStorages) {
StateComponentInfo<?> componentInfo = myComponents.get(componentName);
if (componentInfo == null) {
return false;
}
boolean changedStoragesEmpty = changedStorages.isEmpty();
initComponent(componentInfo, changedStoragesEmpty ? null : changedStorages, changedStoragesEmpty);
return true;
}
@NotNull
protected abstract MessageBus getMessageBus();
@Override
@Nullable
public final Collection<String> reload(@NotNull Collection<? extends StateStorage> changedStorages) {
if (changedStorages.isEmpty()) {
return Collections.emptySet();
}
Set<String> componentNames = new SmartHashSet<>();
for (StateStorage storage : changedStorages) {
try {
// we must update (reload in-memory storage data) even if non-reloadable component will be detected later
// not saved -> user does own modification -> new (on disk) state will be overwritten and not applied
storage.analyzeExternalChangesAndUpdateIfNeed(componentNames);
}
catch (Throwable e) {
LOG.error(e);
}
}
if (componentNames.isEmpty()) {
return Collections.emptySet();
}
Collection<String> notReloadableComponents = getNotReloadableComponents(componentNames);
reinitComponents(componentNames, notReloadableComponents, changedStorages);
return notReloadableComponents.isEmpty() ? null : notReloadableComponents;
}
// used in settings repository plugin
public void reinitComponents(@NotNull Set<String> componentNames,
@NotNull Collection<String> notReloadableComponents,
@NotNull Collection<? extends StateStorage> changedStorages) {
MessageBus messageBus = getMessageBus();
messageBus.syncPublisher(BatchUpdateListener.TOPIC).onBatchUpdateStarted();
try {
for (String componentName : componentNames) {
if (!notReloadableComponents.contains(componentName)) {
reinitComponent(componentName, changedStorages);
}
}
}
finally {
messageBus.syncPublisher(BatchUpdateListener.TOPIC).onBatchUpdateFinished();
}
}
public enum ReloadComponentStoreStatus {
RESTART_AGREED,
RESTART_CANCELLED,
ERROR,
SUCCESS,
}
@NotNull
public static ReloadComponentStoreStatus reloadStore(@NotNull Collection<StateStorage> changedStorages, @NotNull IComponentStore.Reloadable store) {
Collection<String> notReloadableComponents;
boolean willBeReloaded = false;
try {
AccessToken token = WriteAction.start();
try {
notReloadableComponents = store.reload(changedStorages);
}
catch (Throwable e) {
Messages.showWarningDialog(ProjectBundle.message("project.reload.failed", e.getMessage()), ProjectBundle.message("project.reload.failed.title"));
return ReloadComponentStoreStatus.ERROR;
}
finally {
token.finish();
}
if (ContainerUtil.isEmpty(notReloadableComponents)) {
return ReloadComponentStoreStatus.SUCCESS;
}
willBeReloaded = askToRestart(store, notReloadableComponents, changedStorages);
return willBeReloaded ? ReloadComponentStoreStatus.RESTART_AGREED : ReloadComponentStoreStatus.RESTART_CANCELLED;
}
finally {
if (!willBeReloaded) {
for (StateStorage storage : changedStorages) {
if (storage instanceof StateStorageBase) {
((StateStorageBase)storage).enableSaving();
}
}
}
}
}
// used in settings repository plugin
public static boolean askToRestart(@NotNull Reloadable store,
@NotNull Collection<String> notReloadableComponents,
@Nullable Collection<? extends StateStorage> changedStorages) {
StringBuilder message = new StringBuilder();
String storeName = store instanceof IApplicationStore ? "Application" : "Project";
message.append(storeName).append(' ');
message.append("components were changed externally and cannot be reloaded:\n\n");
int count = 0;
for (String component : notReloadableComponents) {
if (count == 10) {
message.append('\n').append("and ").append(notReloadableComponents.size() - count).append(" more").append('\n');
}
else {
message.append(component).append('\n');
count++;
}
}
message.append("\nWould you like to ");
if (store instanceof IApplicationStore) {
message.append(ApplicationManager.getApplication().isRestartCapable() ? "restart" : "shutdown").append(' ');
message.append(ApplicationNamesInfo.getInstance().getProductName()).append('?');
}
else {
message.append("reload project?");
}
if (Messages.showYesNoDialog(message.toString(), storeName + " Files Changed", Messages.getQuestionIcon()) == Messages.YES) {
if (changedStorages != null) {
for (StateStorage storage : changedStorages) {
if (storage instanceof StateStorageBase) {
((StateStorageBase)storage).disableSaving();
}
}
}
return true;
}
return false;
}
}