2 * Copyright 2000-2009 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package com
.intellij
.openapi
.components
.impl
.stores
;
18 import com
.intellij
.application
.options
.PathMacrosCollector
;
19 import com
.intellij
.openapi
.Disposable
;
20 import com
.intellij
.openapi
.components
.*;
21 import com
.intellij
.openapi
.diagnostic
.Logger
;
22 import com
.intellij
.openapi
.extensions
.ExtensionPoint
;
23 import com
.intellij
.openapi
.extensions
.Extensions
;
24 import com
.intellij
.openapi
.options
.StreamProvider
;
25 import com
.intellij
.openapi
.util
.Disposer
;
26 import com
.intellij
.openapi
.util
.JDOMUtil
;
27 import com
.intellij
.openapi
.util
.Pair
;
28 import com
.intellij
.openapi
.util
.WriteExternalException
;
29 import com
.intellij
.openapi
.vfs
.VirtualFile
;
30 import com
.intellij
.util
.containers
.StringInterner
;
31 import com
.intellij
.util
.io
.fs
.IFile
;
32 import org
.jdom
.Attribute
;
33 import org
.jdom
.Document
;
34 import org
.jdom
.Element
;
35 import org
.jetbrains
.annotations
.NonNls
;
36 import org
.jetbrains
.annotations
.NotNull
;
37 import org
.jetbrains
.annotations
.Nullable
;
39 import java
.io
.IOException
;
42 public abstract class XmlElementStorage
implements StateStorage
, Disposable
{
43 @NonNls private static final Set
<String
> OBSOLETE_COMPONENT_NAMES
= new HashSet
<String
>(Arrays
.asList(
46 private static final Logger LOG
= Logger
.getInstance("#com.intellij.openapi.components.impl.stores.XmlElementStorage");
48 @NonNls private static final String COMPONENT
= "component";
49 @NonNls private static final String ATTR_NAME
= "name";
50 @NonNls private static final String NAME
= ATTR_NAME
;
52 protected TrackingPathMacroSubstitutor myPathMacroSubstitutor
;
53 @NotNull private final String myRootElementName
;
54 private Object mySession
;
55 private StorageData myLoadedData
;
56 protected static StringInterner ourInterner
= new StringInterner();
57 protected final StreamProvider myStreamProvider
;
58 protected final String myFileSpec
;
59 private final ComponentRoamingManager myComponentRoamingManager
;
60 protected final boolean myIsProjectSettings
;
61 protected boolean myBlockSavingTheContent
= false;
62 protected Integer myUpToDateHash
;
63 protected Integer myProviderUpToDateHash
;
64 private boolean mySavingDisabled
= false;
66 private final Map
<String
, Element
> myStorageComponentStates
= new TreeMap
<String
, Element
>();
68 private final ComponentVersionProvider myLocalVersionProvider
;
69 private final ComponentVersionProvider myRemoteVersionProvider
;
71 protected Map
<String
, Long
> myProviderVersions
= null;
73 protected ComponentVersionListener myListener
= new ComponentVersionListener(){
74 public void componentStateChanged(String componentName
) {
75 myLocalVersionProvider
.changeVersion(componentName
, System
.currentTimeMillis());
80 protected XmlElementStorage(@Nullable final TrackingPathMacroSubstitutor pathMacroSubstitutor
,
81 @NotNull Disposable parentDisposable
,
82 @NotNull String rootElementName
,
83 StreamProvider streamProvider
,
85 ComponentRoamingManager componentRoamingManager
, ComponentVersionProvider localComponentVersionsProvider
) {
86 myPathMacroSubstitutor
= pathMacroSubstitutor
;
87 myRootElementName
= rootElementName
;
88 myStreamProvider
= streamProvider
;
89 myFileSpec
= fileSpec
;
90 myComponentRoamingManager
= componentRoamingManager
;
91 Disposer
.register(parentDisposable
, this);
92 myIsProjectSettings
= "$PROJECT_FILE$".equals(myFileSpec
) || myFileSpec
.startsWith("$PROJECT_CONFIG_DIR$");
94 myLocalVersionProvider
= localComponentVersionsProvider
;
96 myRemoteVersionProvider
= new ComponentVersionProvider(){
97 public long getVersion(String name
) {
98 if (myProviderVersions
== null) {
99 loadProviderVersions();
102 return myProviderVersions
.containsKey(name
) ? myProviderVersions
.get(name
).longValue() : 0;
106 public void changeVersion(String name
, long version
) {
107 if (myProviderVersions
== null) {
108 loadProviderVersions();
111 myProviderVersions
.put(name
, version
);
117 protected abstract Document
loadDocument() throws StateStorageException
;
120 public synchronized Element
getState(final String componentName
) throws StateStorageException
{
121 final StorageData storageData
= getStorageData(false);
122 final Element state
= storageData
.getState(componentName
);
127 if (!myStorageComponentStates
.containsKey(componentName
)) {
128 myStorageComponentStates
.put(componentName
, state
);
130 storageData
.removeState(componentName
);
136 public boolean hasState(final Object component
, final String componentName
, final Class
<?
> aClass
, final boolean reloadData
) throws StateStorageException
{
137 final StorageData storageData
= getStorageData(reloadData
);
138 return storageData
.hasState(componentName
);
142 public <T
> T
getState(final Object component
, final String componentName
, Class
<T
> stateClass
, @Nullable T mergeInto
) throws StateStorageException
{
143 final Element element
= getState(componentName
);
144 return DefaultStateSerializer
.deserializeState(element
, stateClass
, mergeInto
);
148 protected StorageData
getStorageData(final boolean reloadData
) throws StateStorageException
{
149 if (myLoadedData
!= null && !reloadData
) return myLoadedData
;
151 myLoadedData
= loadData(true, myListener
);
157 protected StorageData
loadData(final boolean useProvidersData
, ComponentVersionListener listener
) throws StateStorageException
{
158 Document document
= loadDocument();
160 StorageData result
= createStorageData();
162 if (document
!= null) {
163 loadState(result
, document
.getRootElement());
166 LOG
.info("Document was not loaded for " + myFileSpec
);
169 if (!myIsProjectSettings
&& useProvidersData
) {
170 for (RoamingType roamingType
: RoamingType
.values()) {
171 if (roamingType
!= RoamingType
.DISABLED
&& roamingType
!= RoamingType
.GLOBAL
) {
173 if (myStreamProvider
.isEnabled()) {
174 final Document sharedDocument
= StorageUtil
.loadDocument(myStreamProvider
.loadContent(myFileSpec
, roamingType
));
176 if (sharedDocument
!= null) {
177 filterComponentsDisabledForRoaming(sharedDocument
.getRootElement(), roamingType
);
178 filterOutOfDateComponents(sharedDocument
.getRootElement());
180 loadState(result
, sharedDocument
.getRootElement());
184 catch (Exception e
) {
196 protected void loadState(final StorageData result
, final Element element
) throws StateStorageException
{
197 if (myPathMacroSubstitutor
!= null) {
198 myPathMacroSubstitutor
.expandPaths(element
);
201 JDOMUtil
.internElement(element
, ourInterner
);
204 result
.load(element
);
205 result
.checkUnknownMacros(myPathMacroSubstitutor
);
207 catch (IOException e
) {
208 throw new StateStorageException(e
);
213 protected StorageData
createStorageData() {
214 return new StorageData(myRootElementName
);
217 public void setDefaultState(final Element element
) {
218 myLoadedData
= createStorageData();
220 loadState(myLoadedData
, element
);
222 catch (StateStorageException e
) {
228 public ExternalizationSession
startExternalization() {
230 final ExternalizationSession session
= new MyExternalizationSession(getStorageData(false).clone(), myListener
);
235 catch (StateStorageException e
) {
236 throw new RuntimeException(e
);
241 public SaveSession
startSave(final ExternalizationSession externalizationSession
) {
242 assert mySession
== externalizationSession
;
244 final SaveSession saveSession
= mySavingDisabled ?
createNullSession() : createSaveSession((MyExternalizationSession
)externalizationSession
);
245 mySession
= saveSession
;
249 private SaveSession
createNullSession() {
250 return new SaveSession(){
251 public void save() throws StateStorageException
{
255 public Set
<String
> analyzeExternalChanges(final Set
<Pair
<VirtualFile
, StateStorage
>> changedFiles
) {
256 return Collections
.emptySet();
259 public Collection
<IFile
> getStorageFilesToSave() throws StateStorageException
{
260 return Collections
.emptySet();
263 public List
<IFile
> getAllStorageFiles() {
264 return Collections
.emptyList();
269 protected abstract MySaveSession
createSaveSession(final MyExternalizationSession externalizationSession
);
271 public void finishSave(final SaveSession saveSession
) {
273 LOG
.assertTrue(mySession
== saveSession
, "mySession=" + mySession
+ " saveSession=" + saveSession
);
279 public void disableSaving() {
280 mySavingDisabled
= true;
283 protected class MyExternalizationSession
implements ExternalizationSession
{
284 private final StorageData myStorageData
;
285 private final ComponentVersionListener myListener
;
287 public MyExternalizationSession(final StorageData storageData
, ComponentVersionListener listener
) {
288 myStorageData
= storageData
;
289 myListener
= listener
;
292 public void setState(final Object component
, final String componentName
, final Object state
, final Storage storageSpec
) throws StateStorageException
{
293 assert mySession
== this;
296 setState(componentName
, DefaultStateSerializer
.serializeState(state
, storageSpec
));
298 catch (WriteExternalException e
) {
303 private synchronized void setState(final String componentName
, final Element element
) {
304 if (element
.getAttributes().isEmpty() && element
.getChildren().isEmpty()) return;
306 myStorageData
.setState(componentName
, element
);
308 Element oldElement
= myStorageComponentStates
.get(componentName
);
310 if (oldElement
!= null && !JDOMUtil
.areElementsEqual(oldElement
, element
)) {
311 myListener
.componentStateChanged(componentName
);
315 myStorageComponentStates
.put(componentName
, (Element
)element
.clone());
323 protected Document
getDocument(StorageData data
) {
324 final Element element
= data
.save();
326 if (myPathMacroSubstitutor
!= null) {
328 myPathMacroSubstitutor
.collapsePaths(element
);
330 myPathMacroSubstitutor
.reset();
334 return new Document(element
);
337 protected abstract class MySaveSession
implements SaveSession
{
338 StorageData myStorageData
;
339 private Document myDocumentToSave
;
341 public MySaveSession(MyExternalizationSession externalizationSession
) {
342 myStorageData
= externalizationSession
.myStorageData
;
346 public final boolean needsSave() throws StateStorageException
{
347 assert mySession
== this;
348 return _needsSave(calcHash());
351 private boolean _needsSave(final Integer hash
) {
352 if (myBlockSavingTheContent
) return false;
353 if (myUpToDateHash
== null) {
355 if (!physicalContentNeedsSave()) {
356 myUpToDateHash
= hash
;
369 if (hash
.intValue() == myUpToDateHash
.intValue()) {
372 if (!physicalContentNeedsSave()) {
373 myUpToDateHash
= hash
;
382 return physicalContentNeedsSave();
388 protected boolean physicalContentNeedsSave() {
392 protected abstract void doSave() throws StateStorageException
;
394 public void clearHash() {
395 myUpToDateHash
= null;
398 protected Integer
calcHash() {
402 public final void save() throws StateStorageException
{
403 assert mySession
== this;
405 if (myBlockSavingTheContent
) return;
407 Integer hash
= calcHash();
410 saveForProviders(hash
);
417 private void saveLocally(final Integer hash
) {
419 if (!isHashUpToDate(hash
)) {
420 if (_needsSave(hash
)) {
426 myUpToDateHash
= hash
;
430 private void saveForProviders(final Integer hash
) {
431 if (myProviderUpToDateHash
== null || !myProviderUpToDateHash
.equals(hash
)) {
433 if (!myIsProjectSettings
) {
434 for (RoamingType roamingType
: RoamingType
.values()) {
435 if (roamingType
!= RoamingType
.DISABLED
) {
437 Document copy
= (Document
)getDocumentToSave().clone();
438 filterComponentsDisabledForRoaming(copy
.getRootElement(), roamingType
);
440 if (copy
.getRootElement().getChildren().size() > 0) {
441 StorageUtil
.sendContent(myStreamProvider
, myFileSpec
, copy
, roamingType
, true);
442 Document versionDoc
= createVersionDocument(copy
);
443 if (versionDoc
.getRootElement().getChildren().size() > 0) {
444 StorageUtil
.sendContent(myStreamProvider
, myFileSpec
+ ".ver", versionDoc
, roamingType
, true);
448 catch (IOException e
) {
457 myProviderUpToDateHash
= hash
;
463 private boolean isHashUpToDate(final Integer hash
) {
464 return myUpToDateHash
!= null && myUpToDateHash
.equals(hash
);
467 public boolean isHashUpToDate() {
468 return isHashUpToDate(calcHash());
471 protected Document
getDocumentToSave() {
472 if (myDocumentToSave
!= null) return myDocumentToSave
;
474 final Element element
= myStorageData
.save();
475 myDocumentToSave
= new Document(element
);
477 if (myPathMacroSubstitutor
!= null) {
478 myPathMacroSubstitutor
.collapsePaths(element
);
481 return myDocumentToSave
;
484 public StorageData
getData() {
485 return myStorageData
;
489 public Set
<String
> analyzeExternalChanges(final Set
<Pair
<VirtualFile
,StateStorage
>> changedFiles
) {
491 Document document
= loadDocument();
493 StorageData storageData
= createStorageData();
495 if (document
!= null) {
496 loadState(storageData
, document
.getRootElement());
497 return storageData
.getDifference(myStorageData
, myPathMacroSubstitutor
);
500 return Collections
.emptySet();
505 catch (StateStorageException e
) {
513 private Document
createVersionDocument(Document copy
) {
514 return new Document(StateStorageManagerImpl
.createComponentVersionsXml(loadVersions(copy
)));
517 private Map
<String
, Long
> loadVersions(Document copy
) {
519 HashMap
<String
, Long
> result
= new HashMap
<String
, Long
>();
521 List list
= copy
.getRootElement().getChildren(COMPONENT
);
522 for (Object o
: list
) {
523 if (o
instanceof Element
) {
524 Element component
= (Element
)o
;
525 String name
= component
.getAttributeValue(ATTR_NAME
);
527 long version
= myLocalVersionProvider
.getVersion(name
);
529 result
.put(name
, version
);
538 public void dispose() {
541 protected static class StorageData
{
542 private final Map
<String
, Element
> myComponentStates
;
543 protected final String myRootElementName
;
544 private Integer myHash
;
546 public StorageData(final String rootElementName
) {
547 myComponentStates
= new TreeMap
<String
, Element
>();
548 myRootElementName
= rootElementName
;
551 protected StorageData(StorageData storageData
) {
552 myRootElementName
= storageData
.myRootElementName
;
553 myComponentStates
= new TreeMap
<String
, Element
>(storageData
.myComponentStates
);
556 protected void load(@NotNull Element rootElement
) throws IOException
{
557 final Element
[] elements
= JDOMUtil
.getElements(rootElement
);
558 for (Element element
: elements
) {
559 if (element
.getName().equals(COMPONENT
)) {
560 final String name
= element
.getAttributeValue(NAME
);
563 LOG
.info("Broken content in file : " + this);
567 if (OBSOLETE_COMPONENT_NAMES
.contains(name
)) continue;
571 if (element
.getAttributes().size() > 1 || !element
.getChildren().isEmpty()) {
572 assert element
.getAttributeValue(NAME
) != null : "No name attribute for component: " + name
+ " in " + this;
574 Element existingElement
= myComponentStates
.get(name
);
576 if (existingElement
!= null) {
577 element
= mergeElements(name
, element
, existingElement
);
580 myComponentStates
.put(name
, element
);
586 private Element
mergeElements(final String name
, final Element element1
, final Element element2
) {
587 ExtensionPoint
<XmlConfigurationMerger
> point
= Extensions
.getRootArea().getExtensionPoint("com.intellij.componentConfigurationMerger");
588 XmlConfigurationMerger
[] mergers
= point
.getExtensions();
589 for (XmlConfigurationMerger merger
: mergers
) {
590 if (merger
.getComponentName().equals(name
)) {
591 return merger
.merge(element1
, element2
);
598 protected Element
save() {
599 Element rootElement
= new Element(myRootElementName
);
601 for (String componentName
: myComponentStates
.keySet()) {
602 assert componentName
!= null;
603 final Element element
= myComponentStates
.get(componentName
);
605 if (element
.getAttribute(NAME
) == null) element
.setAttribute(NAME
, componentName
);
607 rootElement
.addContent((Element
)element
.clone());
614 public Element
getState(final String name
) {
615 final Element e
= myComponentStates
.get(name
);
618 assert e
.getAttributeValue(NAME
) != null : "No name attribute for component: " + name
+ " in " + this;
619 e
.removeAttribute(NAME
);
625 public void removeState(final String componentName
) {
626 myComponentStates
.remove(componentName
);
630 private void setState(@NotNull final String componentName
, final Element element
) {
631 element
.setName(COMPONENT
);
633 //componentName should be first!
634 final List attributes
= new ArrayList(element
.getAttributes());
635 for (Object attribute
: attributes
) {
636 Attribute attr
= (Attribute
)attribute
;
637 element
.removeAttribute(attr
);
640 element
.setAttribute(NAME
, componentName
);
642 for (Object attribute
: attributes
) {
643 Attribute attr
= (Attribute
)attribute
;
644 element
.setAttribute(attr
.getName(), attr
.getValue());
647 myComponentStates
.put(componentName
, element
);
651 public StorageData
clone() {
652 return new StorageData(this);
655 public final int getHash() {
656 if (myHash
== null) {
657 myHash
= computeHash();
659 return myHash
.intValue();
662 protected int computeHash() {
665 for (String name
: myComponentStates
.keySet()) {
666 result
= 31*result
+ name
.hashCode();
667 result
= 31*result
+ JDOMUtil
.getTreeHash(myComponentStates
.get(name
));
673 protected void clearHash() {
677 public Set
<String
> getDifference(final StorageData storageData
, PathMacroSubstitutor substitutor
) {
678 Set
<String
> bothStates
= new HashSet
<String
>(myComponentStates
.keySet());
679 bothStates
.retainAll(storageData
.myComponentStates
.keySet());
681 Set
<String
> diffs
= new HashSet
<String
>();
682 diffs
.addAll(storageData
.myComponentStates
.keySet());
683 diffs
.addAll(myComponentStates
.keySet());
684 diffs
.removeAll(bothStates
);
686 for (String componentName
: bothStates
) {
687 final Element e1
= myComponentStates
.get(componentName
);
688 final Element e2
= storageData
.myComponentStates
.get(componentName
);
690 // some configurations want to collapse path elements in writeExternal so make sure paths are expanded
691 if (substitutor
!= null) {
692 substitutor
.expandPaths(e2
);
695 if (!JDOMUtil
.areElementsEqual(e1
, e2
)) {
696 diffs
.add(componentName
);
704 public boolean isEmpty() {
705 return myComponentStates
.size() == 0;
708 public boolean hasState(final String componentName
) {
709 return myComponentStates
.containsKey(componentName
);
712 public void checkUnknownMacros(TrackingPathMacroSubstitutor pathMacroSubstitutor
) {
713 for (String componentName
: myComponentStates
.keySet()) {
714 final Set
<String
> unknownMacros
= PathMacrosCollector
.getMacroNames(myComponentStates
.get(componentName
));
715 if (!unknownMacros
.isEmpty()) {
716 pathMacroSubstitutor
.addUnknownMacros(componentName
, unknownMacros
);
722 public void resetData(){
726 public void reload(@NotNull final Set
<String
> changedComponents
) throws StateStorageException
{
727 final StorageData storageData
= loadData(false, myListener
);
729 final StorageData oldLoadedData
= myLoadedData
;
731 if (oldLoadedData
!= null) {
732 Set
<String
> componentsToRetain
= new HashSet
<String
>(oldLoadedData
.myComponentStates
.keySet());
733 componentsToRetain
.addAll(changedComponents
);
735 // add empty configuration tags for removed components
736 for (String componentToRetain
: componentsToRetain
) {
737 if (!storageData
.myComponentStates
.containsKey(componentToRetain
) && myStorageComponentStates
.containsKey(componentToRetain
)) {
738 Element emptyElement
= new Element("component");
739 LOG
.info("Create empty component element for " + componentsToRetain
);
740 emptyElement
.setAttribute(NAME
, componentToRetain
);
741 storageData
.myComponentStates
.put(componentToRetain
, emptyElement
);
745 storageData
.myComponentStates
.keySet().retainAll(componentsToRetain
);
748 myLoadedData
= storageData
;
751 private void filterComponentsDisabledForRoaming(final Element element
, final RoamingType roamingType
) {
752 final List components
= element
.getChildren(COMPONENT
);
754 List
<Element
> toDelete
= new ArrayList
<Element
>();
756 for (Object componentObj
: components
) {
757 final Element componentElement
= (Element
)componentObj
;
758 final String nameAttr
= componentElement
.getAttributeValue(NAME
);
760 if (myComponentRoamingManager
.getRoamingType(nameAttr
) != roamingType
) {
761 toDelete
.add(componentElement
);
765 for (Element toDeleteElement
: toDelete
) {
766 element
.removeContent(toDeleteElement
);
770 private void filterOutOfDateComponents(final Element element
) {
771 final List components
= element
.getChildren(COMPONENT
);
773 List
<Element
> toDelete
= new ArrayList
<Element
>();
775 for (Object componentObj
: components
) {
776 final Element componentElement
= (Element
)componentObj
;
777 final String nameAttr
= componentElement
.getAttributeValue(NAME
);
779 if (myRemoteVersionProvider
.getVersion(nameAttr
) <= myLocalVersionProvider
.getVersion(nameAttr
)) {
780 toDelete
.add(componentElement
);
783 myLocalVersionProvider
.changeVersion(nameAttr
, myRemoteVersionProvider
.getVersion(nameAttr
));
787 for (Element toDeleteElement
: toDelete
) {
788 element
.removeContent(toDeleteElement
);
793 private void loadProviderVersions() {
794 myProviderVersions
= new TreeMap
<String
, Long
>();
795 for (RoamingType type
: RoamingType
.values()) {
797 if (myStreamProvider
.isEnabled()) {
799 doc
= StorageUtil
.loadDocument(myStreamProvider
.loadContent(myFileSpec
+ ".ver", type
));
801 catch (IOException e
) {
806 StateStorageManagerImpl
.loadComponentVersions(myProviderVersions
, doc
);