cleanup
[idea/community.git] / platform / core-impl / src / com / intellij / openapi / fileEditor / impl / LoadTextUtil.java
1 /*
2  * Copyright 2000-2016 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.openapi.fileEditor.impl;
17
18 import com.intellij.openapi.diagnostic.Logger;
19 import com.intellij.openapi.fileTypes.BinaryFileDecompiler;
20 import com.intellij.openapi.fileTypes.BinaryFileTypeDecompilers;
21 import com.intellij.openapi.fileTypes.CharsetUtil;
22 import com.intellij.openapi.fileTypes.FileType;
23 import com.intellij.openapi.project.Project;
24 import com.intellij.openapi.util.Key;
25 import com.intellij.openapi.util.Pair;
26 import com.intellij.openapi.util.Trinity;
27 import com.intellij.openapi.util.text.StringUtil;
28 import com.intellij.openapi.vfs.CharsetToolkit;
29 import com.intellij.openapi.vfs.VirtualFile;
30 import com.intellij.openapi.vfs.encoding.EncodingManager;
31 import com.intellij.openapi.vfs.encoding.EncodingRegistry;
32 import com.intellij.testFramework.LightVirtualFile;
33 import com.intellij.util.ArrayUtil;
34 import com.intellij.util.ObjectUtils;
35 import com.intellij.util.text.CharArrayUtil;
36 import org.jetbrains.annotations.Nls;
37 import org.jetbrains.annotations.NotNull;
38 import org.jetbrains.annotations.Nullable;
39
40 import java.io.IOException;
41 import java.nio.ByteBuffer;
42 import java.nio.CharBuffer;
43 import java.nio.charset.Charset;
44 import java.nio.charset.UnsupportedCharsetException;
45
46 public final class LoadTextUtil {
47   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.fileEditor.impl.LoadTextUtil");
48   @Nls private static final String AUTO_DETECTED_FROM_BOM = "auto-detected from BOM";
49
50   private LoadTextUtil() { }
51
52   @NotNull
53   private static Pair<CharSequence, String> convertLineSeparators(@NotNull CharBuffer buffer) {
54     int dst = 0;
55     char prev = ' ';
56     int crCount = 0;
57     int lfCount = 0;
58     int crlfCount = 0;
59
60     final int length = buffer.length();
61     final char[] bufferArray = CharArrayUtil.fromSequenceWithoutCopying(buffer);
62
63     for (int src = 0; src < length; src++) {
64       char c = bufferArray != null ? bufferArray[src]:buffer.charAt(src);
65       switch (c) {
66         case '\r':
67           if(bufferArray != null) bufferArray[dst++] = '\n';
68           else buffer.put(dst++, '\n');
69           crCount++;
70           break;
71         case '\n':
72           if (prev == '\r') {
73             crCount--;
74             crlfCount++;
75           }
76           else {
77             if(bufferArray != null) bufferArray[dst++] = '\n';
78             else buffer.put(dst++, '\n');
79             lfCount++;
80           }
81           break;
82         default:
83           if(bufferArray != null) bufferArray[dst++] = c;
84           else buffer.put(dst++, c);
85           break;
86       }
87       prev = c;
88     }
89
90     String detectedLineSeparator = null;
91     if (crlfCount > crCount && crlfCount > lfCount) {
92       detectedLineSeparator = "\r\n";
93     }
94     else if (crCount > lfCount) {
95       detectedLineSeparator = "\r";
96     }
97     else if (lfCount > 0) {
98       detectedLineSeparator = "\n";
99     }
100
101     CharSequence result = buffer.length() == dst ? buffer : buffer.subSequence(0, dst);
102     return Pair.create(result, detectedLineSeparator);
103   }
104
105   @NotNull
106   private static Charset detectCharset(@NotNull VirtualFile virtualFile, @NotNull byte[] content, @NotNull FileType fileType) {
107     Charset charset = null;
108
109     String charsetName = fileType.getCharset(virtualFile, content);
110     Trinity<Charset,CharsetToolkit.GuessedEncoding, byte[]> guessed = guessFromContent(virtualFile, content, content.length);
111
112     Charset hardCodedCharset = guessed == null ? null : guessed.first;
113     if (charsetName != null) {
114       charset = CharsetToolkit.forName(charsetName);
115     }
116     else if (hardCodedCharset == null) {
117       Charset specifiedExplicitly = EncodingRegistry.getInstance().getEncoding(virtualFile, true);
118       if (specifiedExplicitly != null) {
119         charset = specifiedExplicitly;
120       }
121     }
122     else {
123       charset = hardCodedCharset;
124     }
125
126     if (charset == null) {
127       charset = EncodingRegistry.getInstance().getDefaultCharset();
128     }
129     virtualFile.setCharset(charset);
130     return charset;
131   }
132
133   @NotNull
134   public static Charset detectCharsetAndSetBOM(@NotNull VirtualFile virtualFile, @NotNull byte[] content) {
135     return doDetectCharsetAndSetBOM(virtualFile, content, true, virtualFile.getFileType()).getFirst();
136   }
137
138   @NotNull
139   private static Pair.NonNull<Charset, byte[]> doDetectCharsetAndSetBOM(@NotNull VirtualFile virtualFile, @NotNull byte[] content, boolean saveBOM, @NotNull FileType fileType) {
140     @NotNull Charset charset = virtualFile.isCharsetSet() ? virtualFile.getCharset() : detectCharset(virtualFile, content,fileType);
141     Pair.NonNull<Charset, byte[]> bomAndCharset = getCharsetAndBOM(content, charset);
142     final byte[] bom = bomAndCharset.second;
143     if (saveBOM && bom.length != 0) {
144       virtualFile.setBOM(bom);
145       setCharsetWasDetectedFromBytes(virtualFile, AUTO_DETECTED_FROM_BOM);
146     }
147     return bomAndCharset;
148   }
149
150   private static final boolean GUESS_UTF = Boolean.parseBoolean(System.getProperty("idea.guess.utf.encoding", "true"));
151
152   @Nullable("null means no luck, otherwise it's tuple(guessed encoding, hint about content if was unable to guess, BOM)")
153   public static Trinity<Charset, CharsetToolkit.GuessedEncoding, byte[]> guessFromContent(@NotNull VirtualFile virtualFile, @NotNull byte[] content, int length) {
154     Charset defaultCharset = ObjectUtils.notNull(EncodingManager.getInstance().getEncoding(virtualFile, true), CharsetToolkit.getDefaultSystemCharset());
155     CharsetToolkit toolkit = GUESS_UTF ? new CharsetToolkit(content, defaultCharset) : null;
156     String detectedFromBytes = null;
157     try {
158       if (GUESS_UTF) {
159         toolkit.setEnforce8Bit(true);
160         Charset charset = toolkit.guessFromBOM();
161         if (charset != null) {
162           detectedFromBytes = AUTO_DETECTED_FROM_BOM;
163           byte[] bom = ObjectUtils.notNull(CharsetToolkit.getMandatoryBom(charset), CharsetToolkit.UTF8_BOM);
164           return Trinity.create(charset, null, bom);
165         }
166         CharsetToolkit.GuessedEncoding guessed = toolkit.guessFromContent(length);
167         if (guessed == CharsetToolkit.GuessedEncoding.VALID_UTF8) {
168           detectedFromBytes = "auto-detected from bytes";
169           return Trinity.create(CharsetToolkit.UTF8_CHARSET, guessed, null); //UTF detected, ignore all directives
170         }
171         if (guessed == CharsetToolkit.GuessedEncoding.SEVEN_BIT) {
172           return Trinity.create(null, guessed, null);
173         }
174       }
175       return null;
176     }
177     finally {
178       setCharsetWasDetectedFromBytes(virtualFile, detectedFromBytes);
179     }
180   }
181
182   @NotNull
183   private static Pair.NonNull<Charset,byte[]> getCharsetAndBOM(@NotNull byte[] content, @NotNull Charset charset) {
184     if (charset.name().contains(CharsetToolkit.UTF8) && CharsetToolkit.hasUTF8Bom(content)) {
185       return Pair.createNonNull(charset, CharsetToolkit.UTF8_BOM);
186     }
187     try {
188       Charset fromBOM = CharsetToolkit.guessFromBOM(content);
189       if (fromBOM != null) {
190         return Pair.createNonNull(fromBOM, ObjectUtils.notNull(CharsetToolkit.getMandatoryBom(fromBOM), ArrayUtil.EMPTY_BYTE_ARRAY));
191       }
192     }
193     catch (UnsupportedCharsetException ignore) {
194     }
195
196     return Pair.createNonNull(charset, ArrayUtil.EMPTY_BYTE_ARRAY);
197   }
198
199   public static void changeLineSeparators(@Nullable Project project,
200                                           @NotNull VirtualFile file,
201                                           @NotNull String newSeparator,
202                                           @NotNull Object requestor) throws IOException
203   {
204     CharSequence currentText = getTextByBinaryPresentation(file.contentsToByteArray(), file, true, false);
205     String currentSeparator = detectLineSeparator(file, false);
206     if (newSeparator.equals(currentSeparator)) {
207       return;
208     }
209     String newText = StringUtil.convertLineSeparators(currentText.toString(), newSeparator);
210
211     file.setDetectedLineSeparator(newSeparator);
212     write(project, file, requestor, newText, -1);
213   }
214
215   /**
216    * Overwrites file with text and sets modification stamp and time stamp to the specified values.
217    * <p/>
218    * Normally you should not use this method.
219    *
220    * @param requestor            any object to control who called this method. Note that
221    *                             it is considered to be an external change if {@code requestor} is {@code null}.
222    *                             See {@link com.intellij.openapi.vfs.VirtualFileEvent#getRequestor}
223    * @param newModificationStamp new modification stamp or -1 if no special value should be set @return {@code Writer}
224    * @throws IOException if an I/O error occurs
225    * @see VirtualFile#getModificationStamp()
226    */
227   public static void write(@Nullable Project project,
228                            @NotNull VirtualFile virtualFile,
229                            @NotNull Object requestor,
230                            @NotNull String text,
231                            long newModificationStamp) throws IOException {
232     Charset existing = virtualFile.getCharset();
233     Pair.NonNull<Charset, byte[]> chosen = charsetForWriting(project, virtualFile, text, existing);
234     Charset charset = chosen.first;
235     byte[] buffer = chosen.second;
236     if (!charset.equals(existing)) {
237       virtualFile.setCharset(charset);
238     }
239     setDetectedFromBytesFlagBack(virtualFile, buffer);
240
241     virtualFile.setBinaryContent(buffer, newModificationStamp, -1, requestor);
242   }
243
244   @NotNull
245   private static Pair.NonNull<Charset, byte[]> charsetForWriting(@Nullable Project project,
246                                                          @NotNull VirtualFile virtualFile,
247                                                          @NotNull String text,
248                                                          @NotNull Charset existing) {
249     Charset specified = extractCharsetFromFileContent(project, virtualFile, text);
250     Pair.NonNull<Charset, byte[]> chosen = chooseMostlyHarmlessCharset(existing, specified, text);
251     Charset charset = chosen.first;
252
253     // in case of "UTF-16", OutputStreamWriter sometimes adds BOM on it's own.
254     // see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6800103
255     byte[] bom = virtualFile.getBOM();
256     Charset fromBom = bom == null ? null : CharsetToolkit.guessFromBOM(bom);
257     if (fromBom != null && !fromBom.equals(charset)) {
258       chosen = Pair.createNonNull(fromBom, toBytes(text, fromBom));
259     }
260     return chosen;
261   }
262
263   private static void setDetectedFromBytesFlagBack(@NotNull VirtualFile virtualFile, @NotNull byte[] content) {
264     if (virtualFile.getBOM() == null) {
265       guessFromContent(virtualFile, content, content.length);
266     }
267     else {
268       // prevent file to be reloaded in other encoding after save with BOM
269       setCharsetWasDetectedFromBytes(virtualFile, AUTO_DETECTED_FROM_BOM);
270     }
271   }
272
273   @NotNull
274   public static Pair.NonNull<Charset, byte[]> chooseMostlyHarmlessCharset(@NotNull Charset existing, @NotNull Charset specified, @NotNull String text) {
275     try {
276       if (specified.equals(existing)) {
277         return Pair.createNonNull(specified, toBytes(text, existing));
278       }
279
280       byte[] out = isSupported(specified, text);
281       if (out != null) {
282         return Pair.createNonNull(specified, out); //if explicitly specified encoding is safe, return it
283       }
284       out = isSupported(existing, text);
285       if (out != null) {
286         return Pair.createNonNull(existing, out);   //otherwise stick to the old encoding if it's ok
287       }
288       return Pair.createNonNull(specified, toBytes(text, specified)); //if both are bad there is no difference
289     }
290     catch (RuntimeException e) {
291       return Pair.createNonNull(Charset.defaultCharset(), toBytes(text, null)); //if both are bad and there is no hope, use the default charset
292     }
293   }
294
295   @NotNull
296   private static byte[] toBytes(@NotNull String text, @Nullable Charset charset) throws RuntimeException {
297     //noinspection SSBasedInspection
298     return charset == null ? text.getBytes() : text.getBytes(charset);
299   }
300
301   @Nullable("null means not supported, otherwise it is converted byte stream")
302   private static byte[] isSupported(@NotNull Charset charset, @NotNull String str) {
303     try {
304       if (!charset.canEncode()) return null;
305       byte[] bytes = str.getBytes(charset);
306       if (!str.equals(new String(bytes, charset))) {
307         return null;
308       }
309
310       return bytes;
311     }
312     catch (Exception e) {
313       return null;//wow, some charsets throw NPE inside .getBytes() when unable to encode (JIS_X0212-1990)
314     }
315   }
316
317   @NotNull
318   public static Charset extractCharsetFromFileContent(@Nullable Project project, @NotNull VirtualFile virtualFile, @NotNull CharSequence text) {
319     return ObjectUtils.notNull(charsetFromContentOrNull(project, virtualFile, text), virtualFile.getCharset());
320   }
321
322   @Nullable("returns null if cannot determine from content")
323   public static Charset charsetFromContentOrNull(@Nullable Project project, @NotNull VirtualFile virtualFile, @NotNull CharSequence text) {
324     return CharsetUtil.extractCharsetFromFileContent(project, virtualFile, virtualFile.getFileType(), text);
325   }
326
327   @NotNull
328   public static CharSequence loadText(@NotNull final VirtualFile file) {
329     if (file instanceof LightVirtualFile) {
330       return ((LightVirtualFile)file).getContent();
331     }
332
333     if (file.isDirectory()) {
334       throw new AssertionError("'" + file.getPresentableUrl() + "' is a directory");
335     }
336
337     FileType fileType = file.getFileType();
338     if (fileType.isBinary()) {
339       final BinaryFileDecompiler decompiler = BinaryFileTypeDecompilers.INSTANCE.forFileType(fileType);
340       if (decompiler != null) {
341         CharSequence text = decompiler.decompile(file);
342         try {
343           StringUtil.assertValidSeparators(text);
344         }
345         catch (AssertionError e) {
346           LOG.error(e);
347         }
348         return text;
349       }
350
351       throw new IllegalArgumentException("Attempt to load text for binary file which doesn't have a decompiler plugged in: " +
352                                          file.getPresentableUrl() + ". File type: " + fileType.getName());
353     }
354
355     try {
356       byte[] bytes = file.contentsToByteArray();
357       return getTextByBinaryPresentation(bytes, file);
358     }
359     catch (IOException e) {
360       return ArrayUtil.EMPTY_CHAR_SEQUENCE;
361     }
362   }
363
364   @NotNull
365   public static CharSequence getTextByBinaryPresentation(@NotNull final byte[] bytes, @NotNull VirtualFile virtualFile) {
366     return getTextByBinaryPresentation(bytes, virtualFile, true, true);
367   }
368
369   @NotNull
370   public static CharSequence getTextByBinaryPresentation(@NotNull byte[] bytes,
371                                                          @NotNull VirtualFile virtualFile,
372                                                          boolean saveDetectedSeparators,
373                                                          boolean saveBOM) {
374     return getTextByBinaryPresentation(bytes, virtualFile, saveDetectedSeparators, saveBOM, virtualFile.getFileType());
375   }
376   @NotNull
377   public static CharSequence getTextByBinaryPresentation(@NotNull byte[] bytes,
378                                                          @NotNull VirtualFile virtualFile,
379                                                          boolean saveDetectedSeparators,
380                                                          boolean saveBOM,
381                                                          @NotNull FileType fileType) {
382     Pair.NonNull<Charset, byte[]> pair = doDetectCharsetAndSetBOM(virtualFile, bytes, saveBOM, fileType);
383     Charset charset = pair.getFirst();
384     byte[] bom = pair.getSecond();
385     int offset = bom.length;
386
387     Pair<CharSequence, String> result = convertBytes(bytes, charset, offset);
388     if (saveDetectedSeparators) {
389       virtualFile.setDetectedLineSeparator(result.getSecond());
390     }
391     return result.getFirst();
392   }
393
394   /**
395    * Get detected line separator, if the file never been loaded, is loaded if checkFile parameter is specified.
396    *
397    * @param file      the file to check
398    * @param checkFile if the line separator was not detected before, try to detect it
399    * @return the detected line separator or null
400    */
401   @Nullable
402   public static String detectLineSeparator(@NotNull VirtualFile file, boolean checkFile) {
403     String lineSeparator = getDetectedLineSeparator(file);
404     if (lineSeparator == null && checkFile) {
405       try {
406         getTextByBinaryPresentation(file.contentsToByteArray(), file);
407         lineSeparator = getDetectedLineSeparator(file);
408       }
409       catch (IOException e) {
410         // null will be returned
411       }
412     }
413     return lineSeparator;
414   }
415
416   static String getDetectedLineSeparator(@NotNull VirtualFile file) {
417     return file.getDetectedLineSeparator();
418   }
419
420   @NotNull
421   public static CharSequence getTextByBinaryPresentation(@NotNull byte[] bytes, @NotNull Charset charset) {
422     Pair.NonNull<Charset, byte[]> pair = getCharsetAndBOM(bytes, charset);
423     byte[] bom = pair.getSecond();
424     int offset = bom.length;
425
426     final Pair<CharSequence, String> result = convertBytes(bytes, pair.first, offset);
427     return result.getFirst();
428   }
429
430   // do not need to think about BOM here. it is processed outside
431   @NotNull
432   private static Pair<CharSequence, String> convertBytes(@NotNull byte[] bytes, @NotNull Charset charset, final int startOffset) {
433     ByteBuffer byteBuffer = ByteBuffer.wrap(bytes, startOffset, bytes.length - startOffset);
434
435     CharBuffer charBuffer;
436     try {
437       charBuffer = charset.decode(byteBuffer);
438     }
439     catch (Exception e) {
440       // esoteric charsets can throw any kind of exception
441       charBuffer = CharBuffer.wrap(ArrayUtil.EMPTY_CHAR_ARRAY);
442     }
443     return convertLineSeparators(charBuffer);
444   }
445
446   private static final Key<String> CHARSET_WAS_DETECTED_FROM_BYTES = Key.create("CHARSET_WAS_DETECTED_FROM_BYTES");
447   @Nullable("null if was not detected, otherwise the reason it was")
448   public static String wasCharsetDetectedFromBytes(@NotNull VirtualFile virtualFile) {
449     return virtualFile.getUserData(CHARSET_WAS_DETECTED_FROM_BYTES);
450   }
451
452   public static void setCharsetWasDetectedFromBytes(@NotNull VirtualFile virtualFile,
453                                                     @Nullable("null if was not detected, otherwise the reason it was") String reason) {
454     virtualFile.putUserData(CHARSET_WAS_DETECTED_FROM_BYTES, reason);
455   }
456 }