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