Cleanup (formatting; annotations)
[idea/community.git] / platform / util / src / com / intellij / util / io / URLUtil.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.util.io;
17
18 import com.intellij.openapi.util.Pair;
19 import com.intellij.openapi.util.io.FileUtil;
20 import com.intellij.openapi.util.text.StringUtil;
21 import com.intellij.openapi.vfs.CharsetToolkit;
22 import com.intellij.util.Base64Converter;
23 import gnu.trove.TIntArrayList;
24 import org.jetbrains.annotations.NotNull;
25 import org.jetbrains.annotations.Nullable;
26
27 import java.io.FileNotFoundException;
28 import java.io.FilterInputStream;
29 import java.io.IOException;
30 import java.io.InputStream;
31 import java.net.MalformedURLException;
32 import java.net.URL;
33 import java.util.regex.Matcher;
34 import java.util.regex.Pattern;
35 import java.util.zip.ZipEntry;
36 import java.util.zip.ZipFile;
37
38 import static com.intellij.openapi.util.text.StringUtil.stripQuotesAroundValue;
39
40 public class URLUtil {
41   public static final String SCHEME_SEPARATOR = "://";
42   public static final String FILE_PROTOCOL = "file";
43   public static final String HTTP_PROTOCOL = "http";
44   public static final String JAR_PROTOCOL = "jar";
45   public static final String JAR_SEPARATOR = "!/";
46
47   public static final Pattern DATA_URI_PATTERN = Pattern.compile("data:([^,;]+/[^,;]+)(;charset=[^,;]+)?(;base64)?,(.+)");
48
49   private URLUtil() { }
50
51   /**
52    * Opens a url stream. The semantics is the sames as {@link URL#openStream()}. The
53    * separate method is needed, since jar URLs open jars via JarFactory and thus keep them
54    * mapped into memory.
55    */
56   @NotNull
57   public static InputStream openStream(@NotNull URL url) throws IOException {
58     String protocol = url.getProtocol();
59     return protocol.equals(JAR_PROTOCOL) ? openJarStream(url) : url.openStream();
60   }
61
62   @NotNull
63   public static InputStream openResourceStream(@NotNull URL url) throws IOException {
64     try {
65       return openStream(url);
66     }
67     catch (FileNotFoundException ex) {
68       String protocol = url.getProtocol();
69       String file = null;
70       if (protocol.equals(FILE_PROTOCOL)) {
71         file = url.getFile();
72       }
73       else if (protocol.equals(JAR_PROTOCOL)) {
74         int pos = url.getFile().indexOf("!");
75         if (pos >= 0) {
76           file = url.getFile().substring(pos + 1);
77         }
78       }
79       if (file != null && file.startsWith("/")) {
80         InputStream resourceStream = URLUtil.class.getResourceAsStream(file);
81         if (resourceStream != null) return resourceStream;
82       }
83       throw ex;
84     }
85   }
86
87   @NotNull
88   private static InputStream openJarStream(@NotNull URL url) throws IOException {
89     Pair<String, String> paths = splitJarUrl(url.getFile());
90     if (paths == null) {
91       throw new MalformedURLException(url.getFile());
92     }
93
94     @SuppressWarnings("IOResourceOpenedButNotSafelyClosed") final ZipFile zipFile = new ZipFile(FileUtil.unquote(paths.first));
95     ZipEntry zipEntry = zipFile.getEntry(paths.second);
96     if (zipEntry == null) {
97       zipFile.close();
98       throw new FileNotFoundException("Entry " + paths.second + " not found in " + paths.first);
99     }
100
101     return new FilterInputStream(zipFile.getInputStream(zipEntry)) {
102       @Override
103       public void close() throws IOException {
104         super.close();
105         zipFile.close();
106       }
107     };
108   }
109
110   /**
111    * Splits .jar URL along a separator and strips "jar" and "file" prefixes if any.
112    * Returns a pair of path to a .jar file and entry name inside a .jar, or null if the URL does not contain a separator.
113    * <p/>
114    * E.g. "jar:file:///path/to/jar.jar!/resource.xml" is converted into ["/path/to/jar.jar", "resource.xml"].
115    */
116   @Nullable
117   public static Pair<String, String> splitJarUrl(@NotNull String url) {
118     int pivot = url.indexOf(JAR_SEPARATOR);
119     if (pivot < 0) return null;
120
121     String resourcePath = url.substring(pivot + 2);
122     String jarPath = url.substring(0, pivot);
123
124     if (StringUtil.startsWithConcatenation(jarPath, JAR_PROTOCOL, ":")) {
125       jarPath = jarPath.substring(JAR_PROTOCOL.length() + 1);
126     }
127
128     if (jarPath.startsWith(FILE_PROTOCOL)) {
129       jarPath = jarPath.substring(FILE_PROTOCOL.length());
130       if (jarPath.startsWith(SCHEME_SEPARATOR)) {
131         jarPath = jarPath.substring(SCHEME_SEPARATOR.length());
132       }
133       else if (StringUtil.startsWithChar(jarPath, ':')) {
134         jarPath = jarPath.substring(1);
135       }
136     }
137
138     return Pair.create(jarPath, resourcePath);
139   }
140
141   @NotNull
142   public static String unescapePercentSequences(@NotNull String s) {
143     if (s.indexOf('%') == -1) {
144       return s;
145     }
146
147     StringBuilder decoded = new StringBuilder();
148     final int len = s.length();
149     int i = 0;
150     while (i < len) {
151       char c = s.charAt(i);
152       if (c == '%') {
153         TIntArrayList bytes = new TIntArrayList();
154         while (i + 2 < len && s.charAt(i) == '%') {
155           final int d1 = decode(s.charAt(i + 1));
156           final int d2 = decode(s.charAt(i + 2));
157           if (d1 != -1 && d2 != -1) {
158             bytes.add(((d1 & 0xf) << 4 | d2 & 0xf));
159             i += 3;
160           }
161           else {
162             break;
163           }
164         }
165         if (!bytes.isEmpty()) {
166           final byte[] bytesArray = new byte[bytes.size()];
167           for (int j = 0; j < bytes.size(); j++) {
168             bytesArray[j] = (byte)bytes.getQuick(j);
169           }
170           decoded.append(new String(bytesArray, CharsetToolkit.UTF8_CHARSET));
171           continue;
172         }
173       }
174
175       decoded.append(c);
176       i++;
177     }
178     return decoded.toString();
179   }
180
181   private static int decode(char c) {
182     if ((c >= '0') && (c <= '9')) return c - '0';
183     if ((c >= 'a') && (c <= 'f')) return c - 'a' + 10;
184     if ((c >= 'A') && (c <= 'F')) return c - 'A' + 10;
185     return -1;
186   }
187
188   public static boolean containsScheme(@NotNull String url) {
189     return url.contains(SCHEME_SEPARATOR);
190   }
191
192   public static boolean isDataUri(@NotNull String value) {
193     return !value.isEmpty() && value.startsWith("data:", value.charAt(0) == '"' || value.charAt(0) == '\'' ? 1 : 0);
194   }
195
196   /**
197    * Extracts byte array from given data:URL string.
198    * data:URL will be decoded from base64 if it contains the marker of base64 encoding.
199    *
200    * @param dataUrl data:URL-like string (may be quoted)
201    * @return extracted byte array or {@code null} if it cannot be extracted.
202    */
203   @Nullable
204   public static byte[] getBytesFromDataUri(@NotNull String dataUrl) {
205     Matcher matcher = DATA_URI_PATTERN.matcher(stripQuotesAroundValue(dataUrl));
206     if (matcher.matches()) {
207       try {
208         String content = matcher.group(4);
209         return ";base64".equalsIgnoreCase(matcher.group(3))
210                ? Base64Converter.decode(content.getBytes(CharsetToolkit.UTF8_CHARSET))
211                : content.getBytes(CharsetToolkit.UTF8_CHARSET);
212       }
213       catch (IllegalArgumentException e) {
214         return null;
215       }
216     }
217     return null;
218   }
219 }