Merge remote-tracking branch 'origin/master'
[idea/community.git] / plugins / properties / properties-psi-impl / src / com / intellij / lang / properties / psi / impl / PropertiesFileImpl.java
1 /*
2  * Copyright 2000-2014 JetBrains s.r.o.
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16 package com.intellij.lang.properties.psi.impl;
17
18 import com.intellij.extapi.psi.PsiFileBase;
19 import com.intellij.lang.ASTFactory;
20 import com.intellij.lang.ASTNode;
21 import com.intellij.lang.properties.*;
22 import com.intellij.lang.properties.ResourceBundle;
23 import com.intellij.lang.properties.parsing.PropertiesElementTypes;
24 import com.intellij.lang.properties.psi.PropertiesElementFactory;
25 import com.intellij.lang.properties.psi.PropertiesFile;
26 import com.intellij.lang.properties.psi.Property;
27 import com.intellij.openapi.diagnostic.Logger;
28 import com.intellij.openapi.fileTypes.FileType;
29 import com.intellij.psi.FileViewProvider;
30 import com.intellij.psi.PsiElement;
31 import com.intellij.psi.TokenType;
32 import com.intellij.psi.impl.source.tree.ChangeUtil;
33 import com.intellij.psi.impl.source.tree.TreeElement;
34 import com.intellij.psi.stubs.PsiFileStub;
35 import com.intellij.psi.stubs.StubElement;
36 import com.intellij.psi.stubs.StubTree;
37 import com.intellij.psi.tree.TokenSet;
38 import com.intellij.util.ArrayUtil;
39 import com.intellij.util.IncorrectOperationException;
40 import com.intellij.util.containers.ContainerUtil;
41 import com.intellij.util.containers.MostlySingularMultiMap;
42 import gnu.trove.THashMap;
43 import org.jetbrains.annotations.NonNls;
44 import org.jetbrains.annotations.NotNull;
45 import org.jetbrains.annotations.Nullable;
46
47 import java.util.*;
48 import java.util.stream.Collectors;
49
50 public class PropertiesFileImpl extends PsiFileBase implements PropertiesFile {
51   private static final Logger LOG = Logger.getInstance(PropertiesFileImpl.class);
52   private static final TokenSet PROPERTIES_LIST_SET = TokenSet.create(PropertiesElementTypes.PROPERTIES_LIST);
53   private volatile MostlySingularMultiMap<String,IProperty> myPropertiesMap; //guarded by lock
54   private volatile List<IProperty> myProperties;  //guarded by lock
55   private volatile boolean myAlphaSorted;
56   private final Object lock = new Object();
57
58   public PropertiesFileImpl(FileViewProvider viewProvider) {
59     super(viewProvider, PropertiesLanguage.INSTANCE);
60   }
61
62   @Override
63   @NotNull
64   public FileType getFileType() {
65     return PropertiesFileType.INSTANCE;
66   }
67
68   @NonNls
69   public String toString() {
70     return "Properties file:" + getName();
71   }
72
73   @Override
74   @NotNull
75   public List<IProperty> getProperties() {
76     ensurePropertiesLoaded();
77     return myProperties;
78   }
79
80   private ASTNode getPropertiesList() {
81     return ArrayUtil.getFirstElement(getNode().getChildren(PROPERTIES_LIST_SET));
82   }
83
84   private void ensurePropertiesLoaded() {
85     if (myPropertiesMap != null) return;
86     final StubTree stubTree = getStubTree();
87     List<IProperty> properties;
88     MostlySingularMultiMap<String, IProperty> propertiesMap = new MostlySingularMultiMap<String, IProperty>();
89     if (stubTree != null) {
90       final PsiFileStub root = stubTree.getRoot();
91       final StubElement propertiesList = root.findChildStubByType(PropertiesElementTypes.PROPERTIES_LIST);
92       if (propertiesList != null) {
93         properties = Arrays.stream(propertiesList.getChildrenByType(PropertiesElementTypes.PROPERTY, Property[]::new))
94           .map(IProperty.class::cast)
95           .peek(p -> propertiesMap.add(p.getKey(), p))
96           .collect(Collectors.toList());
97       } else {
98         properties = Collections.emptyList();
99       }
100     } else {
101       final ASTNode[] props = getPropertiesList().getChildren(PropertiesElementTypes.PROPERTIES);
102       properties = new ArrayList<IProperty>(props.length);
103       for (final ASTNode prop : props) {
104         final Property property = (Property)prop.getPsi();
105         String key = property.getUnescapedKey();
106         propertiesMap.add(key, property);
107         properties.add(property);
108       }
109     }
110     final boolean isAlphaSorted = PropertiesImplUtil.isAlphaSorted(properties);
111     synchronized (lock) {
112       if (myPropertiesMap != null) return;
113       myProperties = properties;
114       myPropertiesMap = propertiesMap;
115       myAlphaSorted = isAlphaSorted;
116     }
117   }
118
119   @Override
120   public IProperty findPropertyByKey(@NotNull String key) {
121     ensurePropertiesLoaded();
122     synchronized (lock) {
123       Iterator<IProperty> iterator = myPropertiesMap.get(key).iterator();
124       return iterator.hasNext() ? iterator.next() : null;
125     }
126   }
127
128   @Override
129   @NotNull
130   public List<IProperty> findPropertiesByKey(@NotNull String key) {
131     ensurePropertiesLoaded();
132     synchronized (lock) {
133       return ContainerUtil.collect(myPropertiesMap.get(key).iterator());
134     }
135   }
136
137   @Override
138   @NotNull
139   public ResourceBundle getResourceBundle() {
140     return PropertiesImplUtil.getResourceBundle(this);
141   }
142
143   @Override
144   @NotNull
145   public Locale getLocale() {
146     return PropertiesUtil.getLocale(this);
147   }
148
149   @Override
150   public PsiElement add(@NotNull PsiElement element) throws IncorrectOperationException {
151     if (element instanceof Property) {
152       throw new IncorrectOperationException("Use addProperty() instead");
153     }
154     return super.add(element);
155   }
156
157   @Override
158   @NotNull
159   public PsiElement addProperty(@NotNull IProperty property) throws IncorrectOperationException {
160     final IProperty position = findInsertionPosition(property);
161     return addPropertyAfter(property, position);
162   }
163
164   private IProperty findInsertionPosition(@NotNull IProperty property) {
165     synchronized (lock) {
166       ensurePropertiesLoaded();
167       if (myProperties.isEmpty()) {
168         return null;
169       }
170       if (myAlphaSorted) {
171         final int insertIndex = Collections.binarySearch(myProperties, property, (p1, p2) -> {
172           final String k1 = p1.getKey();
173           final String k2 = p2.getKey();
174           LOG.assertTrue(k1 != null && k2 != null);
175           return String.CASE_INSENSITIVE_ORDER.compare(k1, k2);
176         });
177         return insertIndex == -1 ? null :myProperties.get(insertIndex < 0 ? - insertIndex - 2 : insertIndex);
178       }
179       return myProperties.get(myProperties.size() - 1);
180     }
181   }
182
183   @Override
184   @NotNull
185   public PsiElement addPropertyAfter(@NotNull final IProperty property, @Nullable final IProperty anchor) throws IncorrectOperationException {
186     final TreeElement copy = ChangeUtil.copyToElement(property.getPsiElement());
187     List<IProperty> properties = getProperties();
188     ASTNode anchorBefore = anchor == null ? properties.isEmpty() ? null : properties.get(0).getPsiElement().getNode()
189                            : anchor.getPsiElement().getNode().getTreeNext();
190     if (anchorBefore != null) {
191       if (anchorBefore.getElementType() == TokenType.WHITE_SPACE) {
192         anchorBefore = anchorBefore.getTreeNext();
193       }
194     }
195     if (anchorBefore == null && haveToAddNewLine()) {
196       insertLineBreakBefore(null);
197     }
198     getPropertiesList().addChild(copy, anchorBefore);
199     if (anchorBefore != null) {
200       insertLineBreakBefore(anchorBefore);
201     }
202     return copy.getPsi();
203   }
204
205   @NotNull
206   @Override
207   public IProperty addProperty(String key, String value) {
208     return (IProperty)addProperty(PropertiesElementFactory.createProperty(getProject(), key, value, null));
209   }
210
211   @NotNull
212   @Override
213   public IProperty addPropertyAfter(String key, String value, @Nullable IProperty anchor) {
214     return (IProperty)addPropertyAfter(PropertiesElementFactory.createProperty(getProject(), key, value, null), anchor);
215   }
216
217   private void insertLineBreakBefore(final ASTNode anchorBefore) {
218     getPropertiesList().addChild(ASTFactory.whitespace("\n"), anchorBefore);
219   }
220
221   private boolean haveToAddNewLine() {
222     ASTNode lastChild = getPropertiesList().getLastChildNode();
223     return lastChild != null && !lastChild.getText().endsWith("\n");
224   }
225
226   @Override
227   @NotNull
228   public Map<String, String> getNamesMap() {
229     Map<String, String> result = new THashMap<String, String>();
230     for (IProperty property : getProperties()) {
231       result.put(property.getUnescapedKey(), property.getValue());
232     }
233     return result;
234   }
235
236   @Override
237   public boolean isAlphaSorted() {
238     synchronized (lock) {
239       ensurePropertiesLoaded();
240       return myAlphaSorted;
241     }
242   }
243
244   @Override
245   public void clearCaches() {
246     super.clearCaches();
247
248     synchronized (lock) {
249       myPropertiesMap = null;
250       myProperties = null;
251     }
252   }
253 }