blob: 25c940a36d4544543a7097868f18bb3a2fe43efa [file] [log] [blame]
/*
* Copyright 2000-2013 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.Application;
import com.intellij.openapi.application.ApplicationManager;
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.io.FileUtilRt;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.ObjectUtils;
import com.intellij.util.SmartList;
import com.intellij.util.io.fs.IFile;
import gnu.trove.THashMap;
import gnu.trove.TObjectLongHashMap;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import org.picocontainer.MutablePicoContainer;
import org.picocontainer.PicoContainer;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.ConnectException;
import java.util.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public abstract class StateStorageManagerImpl implements StateStorageManager, Disposable, ComponentVersionProvider {
private static final Logger LOG = Logger.getInstance(StateStorageManagerImpl.class);
private static final boolean ourHeadlessEnvironment;
static {
final Application app = ApplicationManager.getApplication();
ourHeadlessEnvironment = app.isHeadlessEnvironment() || app.isUnitTestMode();
}
private final Map<String, String> myMacros = new LinkedHashMap<String, String>();
private final Lock myStorageLock = new ReentrantLock();
private final Map<String, StateStorage> myStorages = new THashMap<String, StateStorage>();
private final Map<String, StateStorage> myPathToStorage = new THashMap<String, StateStorage>();
private final TrackingPathMacroSubstitutor myPathMacroSubstitutor;
private final String myRootTagName;
private Object mySession;
private final PicoContainer myPicoContainer;
private TObjectLongHashMap<String> myComponentVersions;
private final Object myComponentVersionsLock = new Object();
private String myVersionsFilePath;
private boolean isDirty;
private StreamProvider myStreamProvider;
private final OldStreamProviderManager myOldStreamProvider = new OldStreamProviderManager();
public StateStorageManagerImpl(@Nullable TrackingPathMacroSubstitutor pathMacroSubstitutor,
String rootTagName,
@Nullable Disposable parentDisposable,
PicoContainer picoContainer) {
myPicoContainer = picoContainer;
myRootTagName = rootTagName;
myPathMacroSubstitutor = pathMacroSubstitutor;
if (parentDisposable != null) {
Disposer.register(parentDisposable, this);
}
}
@Override
@SuppressWarnings({"unchecked"})
public TrackingPathMacroSubstitutor getMacroSubstitutor() {
return myPathMacroSubstitutor;
}
@Override
public synchronized void addMacro(String macro, String expansion) {
// avoid hundreds of $MODULE_FILE$ instances
myMacros.put(("$" + macro + "$").intern(), expansion);
}
@Override
@Nullable
public StateStorage getStateStorage(@NotNull final Storage storageSpec) throws StateStorageException {
String key = getStorageSpecId(storageSpec);
myStorageLock.lock();
try {
StateStorage stateStorage = myStorages.get(key);
if (stateStorage == null) {
stateStorage = createStateStorage(storageSpec);
putStorageToMap(key, stateStorage);
}
return stateStorage;
}
finally {
myStorageLock.unlock();
}
}
@Override
@Nullable
public StateStorage getFileStateStorage(final String fileName) {
myStorageLock.lock();
try {
StateStorage stateStorage = myStorages.get(fileName);
if (stateStorage == null) {
stateStorage = createFileStateStorage(fileName);
putStorageToMap(fileName, stateStorage);
}
return stateStorage;
}
finally {
myStorageLock.unlock();
}
}
@Override
public Collection<String> getStorageFileNames() {
myStorageLock.lock();
try {
if (myStorages.isEmpty()) {
return Collections.emptyList();
}
return Collections.unmodifiableCollection(myStorages.keySet());
}
finally {
myStorageLock.unlock();
}
}
private void putStorageToMap(final String key, final StateStorage stateStorage) {
if (stateStorage != null) {
if (stateStorage instanceof FileBasedStorage) {
//fixing problem with 2 macros for the same directory (APP_CONFIG and OPTIONS)
String filePath = ((FileBasedStorage)stateStorage).getFilePath();
if (myPathToStorage.containsKey(filePath)) {
StateStorage existing = myPathToStorage.get(filePath);
myStorages.put(key, existing);
}
else {
myPathToStorage.put(filePath, stateStorage);
myStorages.put(key, stateStorage);
}
}
else {
myStorages.put(key, stateStorage);
}
}
}
@Nullable
private StateStorage createStateStorage(Storage storageSpec) throws StateStorageException {
if (!storageSpec.storageClass().equals(StorageAnnotationsDefaultValues.NullStateStorage.class)) {
String key = UUID.randomUUID().toString();
((MutablePicoContainer)myPicoContainer).registerComponentImplementation(key, storageSpec.storageClass());
return (StateStorage)myPicoContainer.getComponentInstance(key);
}
if (!storageSpec.stateSplitter().equals(StorageAnnotationsDefaultValues.NullStateSplitter.class)) {
return createDirectoryStateStorage(storageSpec.file(), storageSpec.stateSplitter());
}
return createFileStateStorage(storageSpec.file());
}
private static String getStorageSpecId(Storage storageSpec) {
if (!storageSpec.storageClass().equals(StorageAnnotationsDefaultValues.NullStateStorage.class)) {
return storageSpec.storageClass().getName();
}
else {
return storageSpec.file();
}
}
@Override
public void clearStateStorage(@NotNull String file) {
myStorageLock.lock();
try {
myStorages.remove(file);
}
finally {
myStorageLock.unlock();
}
}
@Nullable
private StateStorage createDirectoryStateStorage(String file, Class<? extends StateSplitter> splitterClass) throws StateStorageException {
String expandedFile = expandMacros(file);
if (expandedFile == null) {
myStorages.put(file, null);
return null;
}
final StateSplitter splitter;
try {
splitter = splitterClass.newInstance();
}
catch (InstantiationException e) {
throw new StateStorageException(e);
}
catch (IllegalAccessException e) {
throw new StateStorageException(e);
}
return new DirectoryBasedStorage(myPathMacroSubstitutor, expandedFile, splitter, this, myPicoContainer);
}
@Nullable
private StateStorage createFileStateStorage(@NotNull final String fileSpec) {
String expandedFile = expandMacros(fileSpec);
if (expandedFile == null) {
myStorages.put(fileSpec, null);
return null;
}
String extension = FileUtilRt.getExtension(new File(expandedFile).getName());
if (!ourHeadlessEnvironment && extension.length() == 0) {
throw new IllegalArgumentException("Extension is missing for storage file: " + expandedFile);
}
return new FileBasedStorage(getMacroSubstitutor(fileSpec), getStreamProvider(), expandedFile, fileSpec, myRootTagName, this,
myPicoContainer, ComponentRoamingManager.getInstance(), this) {
@Override
@NotNull
protected StorageData createStorageData() {
return StateStorageManagerImpl.this.createStorageData(fileSpec);
}
};
}
@Override
public long getVersion(String name) {
return getComponentVersions().get(name);
}
@TestOnly
public void changeVersionsFilePath(String newPath) {
myVersionsFilePath = newPath;
synchronized (myComponentVersionsLock) {
myComponentVersions = null;
}
isDirty = false;
}
private TObjectLongHashMap<String> loadVersions() {
TObjectLongHashMap<String> result = new TObjectLongHashMap<String>();
String filePath = getNotNullVersionsFilePath();
if (filePath != null) {
try {
Document document = JDOMUtil.loadDocument(new File(filePath));
loadComponentVersions(result, document);
}
catch (JDOMException e) {
LOG.debug(e);
}
catch (IOException e) {
LOG.debug(e);
}
}
return result;
}
private String getNotNullVersionsFilePath() {
if (myVersionsFilePath == null) {
myVersionsFilePath = getVersionsFilePath();
}
return myVersionsFilePath;
}
public static void loadComponentVersions(TObjectLongHashMap<String> result, Document document) {
List<Element> componentObjects = document.getRootElement().getChildren("component");
for (Element component : componentObjects) {
String name = component.getAttributeValue("name");
String version = component.getAttributeValue("version");
if (name != null && version != null) {
try {
result.put(name, Long.parseLong(version));
}
catch (NumberFormatException ignored) {
}
}
}
}
protected abstract String getVersionsFilePath();
@Override
public void changeVersion(String name, long version) {
getComponentVersions().put(name, version);
isDirty = true;
}
@Nullable
@Override
public StreamProvider getStreamProvider() {
return ObjectUtils.chooseNotNull(myStreamProvider, myOldStreamProvider);
}
protected TrackingPathMacroSubstitutor getMacroSubstitutor(@NotNull final String fileSpec) {
return myPathMacroSubstitutor;
}
protected abstract StorageData createStorageData(String storageSpec);
private static final Pattern MACRO_PATTERN = Pattern.compile("(\\$[^\\$]*\\$)");
@Override
@Nullable
public synchronized String expandMacros(final String file) {
final Matcher matcher = MACRO_PATTERN.matcher(file);
while (matcher.find()) {
String m = matcher.group(1);
if (!myMacros.containsKey(m) || !ApplicationManager.getApplication().isUnitTestMode() && myMacros.get(m) == null) {
throw new IllegalArgumentException("Unknown macro: " + m + " in storage spec: " + file);
}
}
String actualFile = file;
for (String macro : myMacros.keySet()) {
final String replacement = myMacros.get(macro);
if (replacement != null) {
actualFile = StringUtil.replace(actualFile, macro, replacement);
}
}
return actualFile;
}
@NotNull
@Override
public ExternalizationSession startExternalization() {
if (mySession != null) {
LOG.error("Starting duplicate externalization session: " + mySession);
}
ExternalizationSession session = new MyExternalizationSession();
mySession = session;
return session;
}
@NotNull
@Override
public SaveSession startSave(@NotNull final ExternalizationSession externalizationSession) {
assert mySession == externalizationSession;
SaveSession session = createSaveSession(externalizationSession);
mySession = session;
return session;
}
@NotNull
protected MySaveSession createSaveSession(final ExternalizationSession externalizationSession) {
return new MySaveSession((MyExternalizationSession)externalizationSession);
}
@Override
public void finishSave(@NotNull final SaveSession saveSession) {
try {
assert mySession == saveSession : "mySession=" + mySession + " saveSession=" + saveSession;
((MySaveSession)saveSession).finishSave();
}
finally {
mySession = null;
save();
}
}
@Override
public void reset() {
mySession = null;
}
protected class MyExternalizationSession implements ExternalizationSession {
CompoundExternalizationSession myCompoundExternalizationSession = new CompoundExternalizationSession();
@Override
public void setState(@NotNull final Storage[] storageSpecs, @NotNull final Object component, final String componentName, @NotNull final Object state)
throws StateStorageException {
assert mySession == this;
for (Storage storageSpec : storageSpecs) {
StateStorage stateStorage = getStateStorage(storageSpec);
if (stateStorage == null) continue;
final StateStorage.ExternalizationSession extSession = myCompoundExternalizationSession.getExternalizationSession(stateStorage);
extSession.setState(component, componentName, state, storageSpec);
}
}
@Override
public void setStateInOldStorage(@NotNull Object component, @NotNull final String componentName, @NotNull Object state) throws StateStorageException {
assert mySession == this;
StateStorage stateStorage = getOldStorage(component, componentName, StateStorageOperation.WRITE);
if (stateStorage != null) {
myCompoundExternalizationSession.getExternalizationSession(stateStorage).setState(component, componentName, state, null);
}
}
}
@Override
@Nullable
public StateStorage getOldStorage(Object component, String componentName, StateStorageOperation operation) throws StateStorageException {
return getFileStateStorage(getOldStorageSpec(component, componentName, operation));
}
protected abstract String getOldStorageSpec(Object component, final String componentName, final StateStorageOperation operation)
throws StateStorageException;
protected class MySaveSession implements SaveSession {
CompoundSaveSession myCompoundSaveSession;
public MySaveSession(final MyExternalizationSession externalizationSession) {
myCompoundSaveSession = new CompoundSaveSession(externalizationSession.myCompoundExternalizationSession);
}
@Override
@NotNull
public List<IFile> getAllStorageFilesToSave() throws StateStorageException {
assert mySession == this;
return myCompoundSaveSession.getAllStorageFilesToSave();
}
@Override
@NotNull
public List<IFile> getAllStorageFiles() {
return myCompoundSaveSession.getAllStorageFiles();
}
@Override
public void save() throws StateStorageException {
assert mySession == this;
myCompoundSaveSession.save();
}
public void finishSave() {
try {
LOG.assertTrue(mySession == this);
}
finally {
myCompoundSaveSession.finishSave();
}
}
//returns set of component which were changed, null if changes are much more than just component state.
@Override
@Nullable
public Set<String> analyzeExternalChanges(@NotNull final Set<Pair<VirtualFile, StateStorage>> changedFiles) {
Set<String> result = new HashSet<String>();
nextStorage:
for (Pair<VirtualFile, StateStorage> pair : changedFiles) {
final StateStorage stateStorage = pair.second;
final StateStorage.SaveSession saveSession = myCompoundSaveSession.getSaveSession(stateStorage);
if (saveSession == null) continue nextStorage;
final Set<String> s = saveSession.analyzeExternalChanges(changedFiles);
if (s == null) return null;
result.addAll(s);
}
return result;
}
}
@Override
public void dispose() {
}
@Override
public void registerStreamProvider(@SuppressWarnings("deprecation") com.intellij.openapi.options.StreamProvider streamProvider, final RoamingType type) {
synchronized (myOldStreamProvider) {
myOldStreamProvider.myStreamProviders.add(new OldStreamProviderAdapter(streamProvider, type));
}
}
@Override
public void setStreamProvider(@Nullable StreamProvider streamProvider) {
myStreamProvider = streamProvider;
}
public void save() {
if (!isDirty) return;
String filePath = getNotNullVersionsFilePath();
if (filePath != null) {
File dir = new File(filePath).getParentFile();
if (!dir.isDirectory() && !dir.mkdirs()) {
LOG.warn("Unable to create: " + dir);
}
try {
JDOMUtil.writeDocument(new Document(createComponentVersionsXml(getComponentVersions())), filePath, "\n");
isDirty = false;
}
catch (IOException e) {
LOG.info(e);
}
}
}
private TObjectLongHashMap<String> getComponentVersions() {
synchronized (myComponentVersionsLock) {
if (myComponentVersions == null) {
myComponentVersions = loadVersions();
}
return myComponentVersions;
}
}
public static Element createComponentVersionsXml(TObjectLongHashMap<String> versions) {
Element root = new Element("versions");
Object[] componentNames = versions.keys();
Arrays.sort(componentNames);
for (Object key : componentNames) {
String name = (String)key;
long version = versions.get(name);
if (version != 0) {
Element element = new Element("component");
root.addContent(element);
element.setAttribute("name", name);
element.setAttribute("version", String.valueOf(version));
}
}
return root;
}
private static class OldStreamProviderManager extends StreamProvider implements CurrentUserHolder {
private final List<OldStreamProviderAdapter> myStreamProviders = new SmartList<OldStreamProviderAdapter>();
@Override
public boolean isVersioningRequired() {
return true;
}
@Override
public boolean isEnabled() {
for (StreamProvider provider : myStreamProviders) {
if (provider.isEnabled()) {
return true;
}
}
return false;
}
@Override
public boolean isApplicable(@NotNull String fileSpec, @NotNull RoamingType roamingType) {
for (StreamProvider provider : myStreamProviders) {
if (provider.isApplicable(fileSpec, roamingType)) {
return true;
}
}
return false;
}
@Override
public boolean saveContent(@NotNull String fileSpec, @NotNull byte[] content, int size, @NotNull RoamingType roamingType, boolean async) throws IOException {
boolean result = false;
for (StreamProvider streamProvider : myStreamProviders) {
try {
if (streamProvider.isEnabled() && streamProvider.isApplicable(fileSpec, roamingType) && streamProvider.saveContent(fileSpec, content, size, roamingType, async)) {
result = true;
}
}
catch (ConnectException e) {
LOG.debug("Cannot send user profile to server: " + e.getLocalizedMessage());
}
catch (Exception e) {
LOG.debug(e);
}
}
return result;
}
@Override
public InputStream loadContent(@NotNull final String fileSpec, @NotNull final RoamingType roamingType) throws IOException {
for (StreamProvider streamProvider : myStreamProviders) {
try {
if (streamProvider.isEnabled() && streamProvider.isApplicable(fileSpec, roamingType)) {
InputStream content = streamProvider.loadContent(fileSpec, roamingType);
if (content != null) {
return content;
}
}
}
catch (ConnectException e) {
LOG.debug("Cannot send user profile o server: " + e.getLocalizedMessage());
}
catch (Exception e) {
LOG.debug(e);
}
}
return null;
}
@Override
public void deleteFile(@NotNull String fileSpec, @NotNull RoamingType roamingType) {
for (StreamProvider streamProvider : myStreamProviders) {
try {
if (streamProvider.isEnabled() && streamProvider.isApplicable(fileSpec, roamingType)) {
streamProvider.deleteFile(fileSpec, roamingType);
}
}
catch (Exception e) {
LOG.debug(e);
}
}
}
@Override
public String getCurrentUserName() {
for (OldStreamProviderAdapter provider : myStreamProviders) {
if (!provider.isEnabled()) {
continue;
}
String userName = provider.getCurrentUserName();
if (userName != null) {
return userName;
}
}
return null;
}
}
}