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
.application
.ApplicationManager
;
21 import com
.intellij
.openapi
.components
.StateSplitter
;
22 import com
.intellij
.openapi
.components
.StateStorage
;
23 import com
.intellij
.openapi
.components
.Storage
;
24 import com
.intellij
.openapi
.components
.TrackingPathMacroSubstitutor
;
25 import com
.intellij
.openapi
.diagnostic
.Logger
;
26 import com
.intellij
.openapi
.fileTypes
.FileTypeManager
;
27 import com
.intellij
.openapi
.util
.Disposer
;
28 import com
.intellij
.openapi
.util
.JDOMUtil
;
29 import com
.intellij
.openapi
.util
.Pair
;
30 import com
.intellij
.openapi
.util
.WriteExternalException
;
31 import com
.intellij
.openapi
.util
.text
.StringUtil
;
32 import com
.intellij
.openapi
.vfs
.LocalFileSystem
;
33 import com
.intellij
.openapi
.vfs
.VirtualFile
;
34 import com
.intellij
.openapi
.vfs
.VirtualFileAdapter
;
35 import com
.intellij
.openapi
.vfs
.VirtualFileEvent
;
36 import com
.intellij
.openapi
.vfs
.tracker
.VirtualFileTracker
;
37 import com
.intellij
.util
.PairConsumer
;
38 import static com
.intellij
.util
.io
.fs
.FileSystem
.FILE_SYSTEM
;
39 import com
.intellij
.util
.io
.fs
.IFile
;
40 import com
.intellij
.util
.messages
.MessageBus
;
41 import gnu
.trove
.THashMap
;
42 import org
.jdom
.Document
;
43 import org
.jdom
.Element
;
44 import org
.jdom
.JDOMException
;
45 import org
.jetbrains
.annotations
.NonNls
;
46 import org
.jetbrains
.annotations
.NotNull
;
47 import org
.jetbrains
.annotations
.Nullable
;
48 import org
.picocontainer
.PicoContainer
;
51 import java
.io
.IOException
;
54 //todo: support missing plugins
56 //todo: support storage data
57 public class DirectoryBasedStorage
implements StateStorage
, Disposable
{
58 private static final Logger LOG
= Logger
.getInstance("#com.intellij.openapi.components.impl.stores.DirectoryBasedStorage");
60 private final TrackingPathMacroSubstitutor myPathMacroSubstitutor
;
61 private final IFile myDir
;
62 private final StateSplitter mySplitter
;
64 private Object mySession
;
65 private MyStorageData myStorageData
= null;
66 @NonNls private static final String COMPONENT
= "component";
67 @NonNls private static final String NAME
= "name";
69 private static final IFile
[] EMPTY_FILES
= new IFile
[0];
71 private final FileTypeManager myFileTypeManager
;
73 public DirectoryBasedStorage(final TrackingPathMacroSubstitutor pathMacroSubstitutor
,
75 final StateSplitter splitter
,
76 Disposable parentDisposable
,
77 final PicoContainer picoContainer
) {
78 assert dir
.indexOf("$") < 0;
79 myPathMacroSubstitutor
= pathMacroSubstitutor
;
80 myDir
= FILE_SYSTEM
.createFile(dir
);
81 mySplitter
= splitter
;
82 Disposer
.register(parentDisposable
, this);
84 VirtualFileTracker virtualFileTracker
= (VirtualFileTracker
)picoContainer
.getComponentInstanceOfType(VirtualFileTracker
.class);
85 MessageBus messageBus
= (MessageBus
)picoContainer
.getComponentInstanceOfType(MessageBus
.class);
88 if (virtualFileTracker
!= null && messageBus
!= null) {
89 final String path
= myDir
.getAbsolutePath();
90 final String fileUrl
= LocalFileSystem
.PROTOCOL_PREFIX
+ path
.replace(File
.separatorChar
, '/');
93 final Listener listener
= messageBus
.syncPublisher(STORAGE_TOPIC
);
94 virtualFileTracker
.addTracker(fileUrl
, new VirtualFileAdapter() {
95 public void contentsChanged(final VirtualFileEvent event
) {
96 if (!StringUtil
.endsWithIgnoreCase(event
.getFile().getName(), ".xml")) return;
97 listener
.storageFileChanged(event
, DirectoryBasedStorage
.this);
100 public void fileDeleted(final VirtualFileEvent event
) {
101 if (!StringUtil
.endsWithIgnoreCase(event
.getFile().getName(), ".xml")) return;
102 listener
.storageFileChanged(event
, DirectoryBasedStorage
.this);
105 public void fileCreated(final VirtualFileEvent event
) {
106 if (!StringUtil
.endsWithIgnoreCase(event
.getFile().getName(), ".xml")) return;
107 listener
.storageFileChanged(event
, DirectoryBasedStorage
.this);
112 myFileTypeManager
= FileTypeManager
.getInstance();
116 public <T
> T
getState(final Object component
, final String componentName
, Class
<T
> stateClass
, @Nullable T mergeInto
)
117 throws StateStorageException
{
118 if (myStorageData
== null) myStorageData
= loadState();
121 if (!myStorageData
.containsComponent(componentName
)) {
122 return DefaultStateSerializer
.deserializeState(new Element(COMPONENT
), stateClass
, mergeInto
);
125 final List
<Element
> subElements
= new ArrayList
<Element
>();
126 myStorageData
.processComponent(componentName
, new PairConsumer
<IFile
, Element
>() {
127 public void consume(final IFile iFile
, final Element element
) {
128 final List children
= element
.getChildren();
129 assert children
.size() == 1;
130 final Element subElement
= (Element
)children
.get(0);
132 subElements
.add(subElement
);
136 final Element state
= new Element(COMPONENT
);
137 mySplitter
.mergeStatesInto(state
, subElements
.toArray(new Element
[subElements
.size()]));
138 myStorageData
.removeComponent(componentName
);
140 return DefaultStateSerializer
.deserializeState(state
, stateClass
, mergeInto
);
143 private MyStorageData
loadState() throws StateStorageException
{
144 MyStorageData storageData
= new MyStorageData();
145 if (!myDir
.exists()) {
149 final IFile
[] files
= myDir
.listFiles();
151 for (IFile file
: files
) {
152 if (!StringUtil
.endsWithIgnoreCase(file
.getName(), ".xml")) {
153 //do not load system files like .DS_Store on Mac
156 final Document document
= JDOMUtil
.loadDocument(file
);
157 final Element element
= document
.getRootElement();
158 assert element
.getName().equals(COMPONENT
);
160 String componentName
= element
.getAttributeValue(NAME
);
161 assert componentName
!= null;
163 if (myPathMacroSubstitutor
!= null) {
164 myPathMacroSubstitutor
.expandPaths(element
);
166 final Set
<String
> unknownMacros
= PathMacrosCollector
.getMacroNames(element
);
167 myPathMacroSubstitutor
.addUnknownMacros(componentName
, unknownMacros
);
170 storageData
.put(componentName
, file
, element
, true);
173 catch (IOException e
) {
174 throw new StateStorageException(e
);
176 catch (JDOMException e
) {
177 throw new StateStorageException(e
);
184 public boolean hasState(final Object component
, final String componentName
, final Class
<?
> aClass
, final boolean reloadData
) throws StateStorageException
{
185 if (!myDir
.exists()) return false;
190 public ExternalizationSession
startExternalization() {
191 if (myStorageData
== null) {
193 myStorageData
= loadState();
195 catch (StateStorageException e
) {
199 final ExternalizationSession session
= new MyExternalizationSession(myPathMacroSubstitutor
, myStorageData
.clone());
206 public SaveSession
startSave(final ExternalizationSession externalizationSession
) {
207 assert mySession
== externalizationSession
;
209 final MySaveSession session
=
210 new MySaveSession(((MyExternalizationSession
)externalizationSession
).myStorageData
, myPathMacroSubstitutor
);
215 public void finishSave(final SaveSession saveSession
) {
217 LOG
.assertTrue(mySession
== saveSession
);
223 public void reload(final Set
<String
> changedComponents
) throws StateStorageException
{
224 myStorageData
= loadState();
227 public void dispose() {
230 private class MySaveSession
implements SaveSession
{
231 private final MyStorageData myStorageData
;
232 private final TrackingPathMacroSubstitutor myPathMacroSubstitutor
;
234 private MySaveSession(final MyStorageData storageData
, final TrackingPathMacroSubstitutor pathMacroSubstitutor
) {
235 myStorageData
= storageData
;
236 myPathMacroSubstitutor
= pathMacroSubstitutor
;
239 public void save() throws StateStorageException
{
240 assert mySession
== this;
241 final Set
<String
> currentNames
= new HashSet
<String
>();
243 IFile
[] children
= myDir
.exists() ? myDir
.listFiles() : EMPTY_FILES
;
244 for (IFile child
: children
) {
245 final String fileName
= child
.getName();
246 if (!myFileTypeManager
.isFileIgnored(fileName
)) {
247 currentNames
.add(fileName
);
251 myStorageData
.process(new StorageDataProcessor() {
252 public void process(final String componentName
, final IFile file
, final Element element
) {
253 currentNames
.remove(file
.getName());
255 if (myPathMacroSubstitutor
!= null) {
256 myPathMacroSubstitutor
.collapsePaths(element
);
259 if (file
.getTimeStamp() <= myStorageData
.getLastTimeStamp()) {
260 if (!myDir
.exists()) {
261 myDir
.createParentDirs();
265 StorageUtil
.save(file
, element
, MySaveSession
.this);
266 myStorageData
.updateLastTimestamp(file
);
271 ApplicationManager
.getApplication().runWriteAction(new Runnable() {
273 if (myDir
.exists()) {
274 for (String name
: currentNames
) {
275 IFile child
= myDir
.getChild(name
);
277 if (child
.getTimeStamp() > myStorageData
.getLastTimeStamp()) {
278 // do not touch new files during VC update (which aren't read yet)
279 // now got an opposite problem: file is recreated if was removed by VC during update.
283 final VirtualFile virtualFile
= StorageUtil
.getVirtualFile(child
);
284 if (virtualFile
!= null) {
286 LOG
.debug("Removing configuration file: " + virtualFile
.getPresentableUrl());
287 virtualFile
.delete(MySaveSession
.this);
289 catch (IOException e
) {
298 myStorageData
.clear();
302 public Set
<String
> analyzeExternalChanges(final Set
<Pair
<VirtualFile
, StateStorage
>> changedFiles
) {
303 boolean containsSelf
= false;
305 for (Pair
<VirtualFile
, StateStorage
> pair
: changedFiles
) {
306 if (pair
.second
== DirectoryBasedStorage
.this) {
307 VirtualFile file
= pair
.first
;
308 if ("xml".equalsIgnoreCase(file
.getExtension())) {
315 if (!containsSelf
) return Collections
.emptySet();
317 if (myStorageData
.getComponentNames().size() == 0) {
318 // no state yet, so try to initialize it now
319 final MyStorageData storageData
= loadState();
320 return new HashSet
<String
>(storageData
.getComponentNames());
323 return new HashSet
<String
>(myStorageData
.getComponentNames());
326 public Collection
<IFile
> getStorageFilesToSave() throws StateStorageException
{
327 assert mySession
== this;
329 if (!myDir
.exists()) return getAllStorageFiles();
330 assert myDir
.isDirectory();
332 final List
<IFile
> filesToSave
= new ArrayList
<IFile
>();
334 IFile
[] children
= myDir
.listFiles();
335 final Set
<String
> currentChildNames
= new HashSet
<String
>();
336 for (IFile child
: children
) {
337 if (!myFileTypeManager
.isFileIgnored(child
.getName())) currentChildNames
.add(child
.getName());
340 myStorageData
.process(new StorageDataProcessor() {
341 public void process(final String componentName
, final IFile file
, final Element element
) {
342 if (currentChildNames
.contains(file
.getName())) {
343 currentChildNames
.remove(file
.getName());
345 if (myPathMacroSubstitutor
!= null) {
346 myPathMacroSubstitutor
.collapsePaths(element
);
349 if (!StorageUtil
.contentEquals(element
, file
)) {
350 filesToSave
.add(file
);
357 for (String childName
: currentChildNames
) {
358 final IFile child
= myDir
.getChild(childName
);
359 filesToSave
.add(child
);
365 public List
<IFile
> getAllStorageFiles() {
366 return new ArrayList
<IFile
>(myStorageData
.getAllStorageFiles().keySet());
370 private interface StorageDataProcessor
{
371 void process(String componentName
, IFile file
, Element element
);
374 private static class MyStorageData
{
375 private Map
<String
, Map
<IFile
, Element
>> myStates
= new HashMap
<String
, Map
<IFile
, Element
>>();
376 private long myLastTimestamp
= 0;
377 private MyStorageData myOriginalData
;
379 public Set
<String
> getComponentNames() {
380 return myStates
.keySet();
383 public void put(final String componentName
, final IFile file
, final Element element
, final boolean updateTimestamp
) {
384 LOG
.assertTrue(componentName
!= null, String
.format("Component name should not be null for file: %s", file
== null ?
"NULL!" : file
.getPath()));
386 Map
<IFile
, Element
> stateMap
= myStates
.get(componentName
);
387 if (stateMap
== null) {
388 stateMap
= new HashMap
<IFile
, Element
>();
389 myStates
.put(componentName
, stateMap
);
392 stateMap
.put(file
, element
);
393 if (updateTimestamp
) updateLastTimestamp(file
);
396 public void updateLastTimestamp(final IFile file
) {
397 myLastTimestamp
= Math
.max(myLastTimestamp
, file
.getTimeStamp());
398 if (myOriginalData
!= null) myOriginalData
.myLastTimestamp
= myLastTimestamp
;
401 public long getLastTimeStamp() {
402 return myLastTimestamp
;
405 public Map
<IFile
, Long
> getAllStorageFiles() {
406 final Map
<IFile
, Long
> allStorageFiles
= new THashMap
<IFile
, Long
>();
407 process(new StorageDataProcessor() {
408 public void process(final String componentName
, final IFile file
, final Element element
) {
409 allStorageFiles
.put(file
, file
.getTimeStamp());
413 return allStorageFiles
;
416 public void processComponent(@NotNull final String componentName
, @NotNull final PairConsumer
<IFile
, Element
> consumer
) {
417 final Map
<IFile
, Element
> map
= myStates
.get(componentName
);
419 for (IFile file
: map
.keySet()) {
420 consumer
.consume(file
, map
.get(file
));
425 public void process(@NotNull final StorageDataProcessor processor
) {
426 for (final String componentName
: myStates
.keySet()) {
427 processComponent(componentName
, new PairConsumer
<IFile
, Element
>() {
428 public void consume(final IFile iFile
, final Element element
) {
429 processor
.process(componentName
, iFile
, element
);
435 protected MyStorageData
clone() {
436 final MyStorageData result
= new MyStorageData();
437 result
.myStates
= new HashMap
<String
, Map
<IFile
, Element
>>(myStates
);
438 result
.myLastTimestamp
= myLastTimestamp
;
439 result
.myOriginalData
= this;
443 public void clear() {
445 myOriginalData
= null;
448 public boolean containsComponent(final String componentName
) {
449 return myStates
.get(componentName
) != null;
452 public void removeComponent(final String componentName
) {
453 myStates
.remove(componentName
);
457 private class MyExternalizationSession
implements ExternalizationSession
{
458 private final MyStorageData myStorageData
;
460 private MyExternalizationSession(final TrackingPathMacroSubstitutor pathMacroSubstitutor
, final MyStorageData storageData
) {
461 myStorageData
= storageData
;
464 public void setState(final Object component
, final String componentName
, final Object state
, final Storage storageSpec
)
465 throws StateStorageException
{
466 assert mySession
== this;
467 setState(componentName
, state
, storageSpec
);
470 private void setState(final String componentName
, Object state
, final Storage storageSpec
) throws StateStorageException
{
472 final Element element
= DefaultStateSerializer
.serializeState(state
, storageSpec
);
474 final List
<Pair
<Element
, String
>> states
= mySplitter
.splitState(element
);
475 for (Pair
<Element
, String
> pair
: states
) {
476 Element e
= pair
.first
;
477 String name
= pair
.second
;
479 Element statePart
= new Element(COMPONENT
);
480 statePart
.setAttribute(NAME
, componentName
);
482 statePart
.addContent(e
);
484 myStorageData
.put(componentName
, myDir
.getChild(name
), statePart
, false);
487 catch (WriteExternalException e
) {
488 throw new StateStorageException(e
);