/* * 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.application.Application; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.components.*; import com.intellij.openapi.components.StateStorage.SaveSession; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.Couple; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; import com.intellij.util.PathUtilRt; import com.intellij.util.ReflectionUtil; import com.intellij.util.SmartList; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.messages.MessageBus; import gnu.trove.THashMap; import org.jdom.Element; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.picocontainer.PicoContainer; 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 { 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<>(); private final Lock myStorageLock = new ReentrantLock(); private final Map<String, StateStorage> myStorages = new THashMap<>(); private final TrackingPathMacroSubstitutor myPathMacroSubstitutor; private final String myRootTagName; private final PicoContainer myPicoContainer; private StreamProvider myStreamProvider; 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); } } @NotNull protected abstract String getConfigurationMacro(boolean directorySpec); @NotNull @SuppressWarnings("deprecation") private String buildFileSpec(@NotNull Storage storage) { boolean directorySpec = !storage.stateSplitter().equals(StateSplitterEx.class); String file = storage.file(); if (!StringUtil.isEmpty(file)) { return file; } String value = storage.value(); if (value.isEmpty()) { LOG.error("Storage.value() is empty"); return StoragePathMacros.DEFAULT_FILE; } if (value.equals(StoragePathMacros.WORKSPACE_FILE)) { return value; } return getConfigurationMacro(directorySpec) + "/" + value + (directorySpec ? "/" : ""); } @NotNull private StateStorage createStateStorage(@NotNull Storage storageSpec) { if (!storageSpec.stateSplitter().equals(StateSplitterEx.class)) { StateSplitterEx splitter = ReflectionUtil.newInstance(storageSpec.stateSplitter()); return new DirectoryBasedStorage(myPathMacroSubstitutor, expandMacros(buildFileSpec(storageSpec)), splitter, this, createStorageTopicListener()); } else { return createFileStateStorage(buildFileSpec(storageSpec), storageSpec.roamingType()); } } @Override public TrackingPathMacroSubstitutor getMacroSubstitutor() { return myPathMacroSubstitutor; } @Override public synchronized void addMacro(@NotNull String macro, @NotNull String expansion) { assert !macro.isEmpty(); // backward compatibility if (macro.charAt(0) != '$') { LOG.warn("Add macros instead of macro name: " + macro); expansion = '$' + macro + '$'; } myMacros.put(macro, expansion); } @Override @NotNull public StateStorage getStateStorage(@NotNull Storage storageSpec) { String key = buildFileSpec(storageSpec); myStorageLock.lock(); try { StateStorage stateStorage = myStorages.get(key); if (stateStorage == null) { stateStorage = createStateStorage(storageSpec); myStorages.put(key, stateStorage); } return stateStorage; } finally { myStorageLock.unlock(); } } @Nullable @Override public StateStorage getStateStorage(@NotNull String fileSpec, @NotNull RoamingType roamingType) { myStorageLock.lock(); try { StateStorage stateStorage = myStorages.get(fileSpec); if (stateStorage == null) { stateStorage = createFileStateStorage(fileSpec, roamingType); myStorages.put(fileSpec, stateStorage); } return stateStorage; } finally { myStorageLock.unlock(); } } @NotNull @Override public Couple<Collection<FileBasedStorage>> getCachedFileStateStorages(@NotNull Collection<String> changed, @NotNull Collection<String> deleted) { myStorageLock.lock(); try { return Couple.of(getCachedFileStorages(changed), getCachedFileStorages(deleted)); } finally { myStorageLock.unlock(); } } @NotNull private Collection<FileBasedStorage> getCachedFileStorages(@NotNull Collection<String> fileSpecs) { if (fileSpecs.isEmpty()) { return Collections.emptyList(); } List<FileBasedStorage> result = null; for (String fileSpec : fileSpecs) { StateStorage storage = myStorages.get(fileSpec); if (storage instanceof FileBasedStorage) { if (result == null) { result = new SmartList<>(); } result.add((FileBasedStorage)storage); } } return result == null ? Collections.<FileBasedStorage>emptyList() : result; } @NotNull @Override public Collection<String> getStorageFileNames() { myStorageLock.lock(); try { return myStorages.keySet(); } finally { myStorageLock.unlock(); } } @Override public void clearStateStorage(@NotNull String file) { myStorageLock.lock(); try { myStorages.remove(file); } finally { myStorageLock.unlock(); } } @NotNull private StateStorage createFileStateStorage(@NotNull String fileSpec, @Nullable RoamingType roamingType) { String filePath = FileUtil.toSystemIndependentName(expandMacros(fileSpec)); if (!ourHeadlessEnvironment && PathUtilRt.getFileName(filePath).lastIndexOf('.') < 0) { throw new IllegalArgumentException("Extension is missing for storage file: " + filePath); } if (roamingType == RoamingType.PER_USER && fileSpec.equals(StoragePathMacros.WORKSPACE_FILE)) { roamingType = RoamingType.DISABLED; } beforeFileBasedStorageCreate(); return new FileBasedStorage(filePath, fileSpec, roamingType, getMacroSubstitutor(fileSpec), myRootTagName, StateStorageManagerImpl.this, createStorageTopicListener(), myStreamProvider) { @Override @NotNull protected StorageData createStorageData() { return StateStorageManagerImpl.this.createStorageData(myFileSpec, getFilePath()); } @Override protected boolean isUseXmlProlog() { return StateStorageManagerImpl.this.isUseXmlProlog(); } }; } @Nullable protected StateStorage.Listener createStorageTopicListener() { MessageBus messageBus = (MessageBus)myPicoContainer.getComponentInstanceOfType(MessageBus.class); return messageBus == null ? null : messageBus.syncPublisher(StateStorage.STORAGE_TOPIC); } protected boolean isUseXmlProlog() { return true; } protected void beforeFileBasedStorageCreate() { } @Nullable @Override public final StreamProvider getStreamProvider() { return myStreamProvider; } protected TrackingPathMacroSubstitutor getMacroSubstitutor(@NotNull final String fileSpec) { return myPathMacroSubstitutor; } protected abstract StorageData createStorageData(@NotNull String fileSpec, @NotNull String filePath); private static final Pattern MACRO_PATTERN = Pattern.compile("(\\$[^\\$]*\\$)"); @Override @NotNull public synchronized String expandMacros(@NotNull String file) { Matcher matcher = MACRO_PATTERN.matcher(file); while (matcher.find()) { String m = matcher.group(1); if (!myMacros.containsKey(m)) { throw new IllegalArgumentException("Unknown macro: " + m + " in storage file spec: " + file); } } String expanded = file; for (String macro : myMacros.keySet()) { expanded = StringUtil.replace(expanded, macro, myMacros.get(macro)); } return expanded; } @NotNull @Override public String collapseMacros(@NotNull String path) { String result = path; for (String macro : myMacros.keySet()) { result = StringUtil.replace(result, myMacros.get(macro), macro); } return result; } @NotNull @Override public ExternalizationSession startExternalization() { return new StateStorageManagerExternalizationSession(); } protected class StateStorageManagerExternalizationSession implements ExternalizationSession { final Map<StateStorage, StateStorage.ExternalizationSession> mySessions = new LinkedHashMap<>(); @Override public void setState(@NotNull Storage[] storageSpecs, @NotNull Object component, @NotNull String componentName, @NotNull Object state) { for (Storage storageSpec : storageSpecs) { StateStorage stateStorage = getStateStorage(storageSpec); StateStorage.ExternalizationSession session = getExternalizationSession(stateStorage); if (session != null) { // empty element as null state, so, will be deleted session.setState(component, componentName, storageSpec.deprecated() ? new Element("empty") : state, storageSpec); } } } @Nullable private StateStorage.ExternalizationSession getExternalizationSession(@NotNull StateStorage stateStorage) { StateStorage.ExternalizationSession session = mySessions.get(stateStorage); if (session == null) { session = stateStorage.startExternalization(); if (session != null) { mySessions.put(stateStorage, session); } } return session; } @NotNull @Override public List<SaveSession> createSaveSessions() { if (mySessions.isEmpty()) { return Collections.emptyList(); } List<SaveSession> saveSessions = null; Collection<StateStorage.ExternalizationSession> externalizationSessions = mySessions.values(); for (StateStorage.ExternalizationSession session : externalizationSessions) { SaveSession saveSession = session.createSaveSession(); if (saveSession != null) { if (saveSessions == null) { if (externalizationSessions.size() == 1) { return Collections.singletonList(saveSession); } saveSessions = new SmartList<>(); } saveSessions.add(saveSession); } } return ContainerUtil.notNullize(saveSessions); } } @Override public void dispose() { } @Override public void setStreamProvider(@Nullable StreamProvider streamProvider) { myStreamProvider = streamProvider; } }