Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Side by Side Diff: adblockplussbrowser/src/main/java/org/adblockplus/sbrowser/contentblocker/engine/Subscription.java

Issue 29603697: Issue 5931 - Create tests for util package (Closed)
Patch Set: Created Nov. 10, 2017, 2:23 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 /*
2 * This file is part of Adblock Plus <https://adblockplus.org/>,
3 * Copyright (C) 2006-present eyeo GmbH
4 *
5 * Adblock Plus is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License version 3 as
7 * published by the Free Software Foundation.
8 *
9 * Adblock Plus is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 package org.adblockplus.sbrowser.contentblocker.engine;
19
20 import java.io.BufferedInputStream;
21 import java.io.BufferedOutputStream;
22 import java.io.BufferedReader;
23 import java.io.DataInputStream;
24 import java.io.DataOutputStream;
25 import java.io.File;
26 import java.io.FileInputStream;
27 import java.io.FileOutputStream;
28 import java.io.IOException;
29 import java.io.StringReader;
30 import java.net.URL;
31 import java.nio.charset.StandardCharsets;
32 import java.security.MessageDigest;
33 import java.security.NoSuchAlgorithmException;
34 import java.util.ArrayList;
35 import java.util.Arrays;
36 import java.util.Collection;
37 import java.util.Collections;
38 import java.util.HashMap;
39 import java.util.HashSet;
40 import java.util.List;
41 import java.util.Locale;
42 import java.util.Map;
43 import java.util.Map.Entry;
44 import java.util.zip.GZIPInputStream;
45 import java.util.zip.GZIPOutputStream;
46
47 import android.text.TextUtils;
48 import android.text.format.DateUtils;
49 import android.util.Log;
50
51 /**
52 * Simple subscription representation.
53 */
54 final class Subscription
55 {
56 private static final String TAG = Subscription.class.getSimpleName();
57 public static final String KEY_TITLE = "title";
58 public static final String KEY_VERSION = "version";
59 public static final String KEY_HTTP_ETAG = "_etag";
60 public static final String KEY_HTTP_LAST_MODIFIED = "_last_modified";
61 public static final String KEY_UPDATE_TIMESTAMP = "_update_timestamp";
62 public static final String KEY_TRIED_UPDATE_TIMESTAMP = "_tried_update_timesta mp";
63 public static final String KEY_DOWNLOAD_COUNT = "_download_count";
64 public static final String KEY_ENABLED = "_enabled";
65 public static final String KEY_META_HASH = "_meta_hash";
66
67 private static final long MINIMAL_DOWNLOAD_INTERVAL = DateUtils.HOUR_IN_MILLIS / 4;
68 private static final long DOWNLOAD_RETRY_INTERVAL = DateUtils.HOUR_IN_MILLIS;
69
70 /**
71 * List of meta keys that are allowed to import from a downloaded
72 * subscription.
73 */
74 private static final String[] ALLOWED_META_KEYS_ARRAY =
75 {
76 "checksum", KEY_VERSION, KEY_TITLE, "last modified", "expires", "homep age", "licence"
77 };
78 private static final HashSet<String> ALLOWED_META_KEYS =
79 new HashSet<>(Arrays.asList(ALLOWED_META_KEYS_ARRAY));
80
81 private static final Locale LOCALE_EN = Locale.ENGLISH;
82
83 private final long updateInterval = DateUtils.DAY_IN_MILLIS
84 + (long) (DateUtils.HOUR_IN_MILLIS * 8. * Math.random());
85
86 private final URL url;
87 private final Type type;
88 private final HashMap<String, String> meta = new HashMap<>();
89 private final HashSet<String> filters = new HashSet<>();
90
91 private boolean metaDataValid = true;
92 private boolean filtersValid = true;
93
94 /**
95 * Subscription type.
96 *
97 * @author René Jeschke &lt;rene@adblockplus.org&gt;
98 */
99 public enum Type
100 {
101 /**
102 * Initiated from an URL, can be automatically downloaded.
103 */
104 DOWNLOADABLE,
105 /**
106 * User defined filters or exceptions.
107 */
108 USER
109 }
110
111 /**
112 * Creates a subscription. The type gets determined by {@code url} being
113 * {@code null} or not.
114 *
115 * @param url
116 * @see Subscription.Type
117 */
118 private Subscription(final URL url)
119 {
120 this.url = url;
121 this.type = url != null ? Type.DOWNLOADABLE : Type.USER;
122 }
123
124 /**
125 * Creates a {@code USER} subscription.
126 *
127 * @see Subscription.Type
128 */
129 private Subscription()
130 {
131 this.url = null;
132 this.type = Type.USER;
133 }
134
135 public boolean isMetaDataValid()
136 {
137 return this.metaDataValid;
138 }
139
140 public boolean isFiltersValid()
141 {
142 return this.filtersValid;
143 }
144
145 static long parseLong(final String number)
146 {
147 try
148 {
149 return Long.parseLong(number);
150 }
151 catch (final NumberFormatException nfe)
152 {
153 return 0;
154 }
155 }
156
157 public long getVersion()
158 {
159 return parseLong(this.getMeta(KEY_VERSION, "0"));
160 }
161
162 public long getDownloadCount()
163 {
164 return parseLong(this.getMeta(KEY_DOWNLOAD_COUNT, "0"));
165 }
166
167 public long getLastUpdateTimestamp()
168 {
169 return parseLong(this.getMeta(KEY_UPDATE_TIMESTAMP, "0"));
170 }
171
172 public long getLastTriedUpdateTimestamp()
173 {
174 return parseLong(this.getMeta(KEY_TRIED_UPDATE_TIMESTAMP, "0"));
175 }
176
177 public boolean shouldUpdate(final boolean forced)
178 {
179 final long now = System.currentTimeMillis();
180 final long lastUpdate = this.getLastUpdateTimestamp();
181 final long lastTry = this.getLastTriedUpdateTimestamp();
182
183 if (forced)
184 {
185 return now - Math.max(lastUpdate, lastTry) > MINIMAL_DOWNLOAD_INTERVAL;
186 }
187
188 if (lastTry > lastUpdate)
189 {
190 return now - lastTry > DOWNLOAD_RETRY_INTERVAL;
191 }
192 else
193 {
194 return now - lastUpdate > this.updateInterval;
195 }
196 }
197
198 /**
199 * @return the download URL, null for a {@code USER} subscription.
200 */
201 public URL getURL()
202 {
203 return this.url;
204 }
205
206 /**
207 * @return the type of this subscription
208 * @see Subscription.Type
209 */
210 public Type getType()
211 {
212 return this.type;
213 }
214
215 /**
216 * Retrieves a meta data entry
217 *
218 * @param key
219 * gets converted to all lower case
220 * @param defaultValue
221 * @return the meta data or {@code defaultValue} if not defined
222 */
223 public String getMeta(final String key, final String defaultValue)
224 {
225 final String value = this.meta.get(key.toLowerCase(LOCALE_EN));
226 return value != null ? value : defaultValue;
227 }
228
229 /**
230 * Retrieves a meta data entry
231 *
232 * @param key
233 * gets converted to all lower case
234 * @return the meta data or {@code null} if not defined
235 */
236 public String getMeta(final String key)
237 {
238 return this.meta.get(key.toLowerCase(LOCALE_EN));
239 }
240
241 public String putMeta(final String key, final String value)
242 {
243 return this.meta.put(key, value);
244 }
245
246 public String getTitle()
247 {
248 final String title = this.getMeta(KEY_TITLE);
249 return title != null ? title : this.url.toString();
250 }
251
252 /**
253 * Creates a {@code DOWNLOADABLE} subscription.
254 *
255 * @param url
256 * the update URL
257 * @param lines
258 * @return the subscription
259 * @see Subscription.Type
260 */
261 public static Subscription create(final URL url, final List<String> lines)
262 {
263 final Subscription sub = new Subscription(url);
264 sub.parseLines(lines);
265 return sub;
266 }
267
268 /**
269 * Creates a {@code DOWNLOADABLE} subscription.
270 *
271 * @param url
272 * the update URL
273 * @return the subscription
274 * @see Subscription.Type
275 */
276 public static Subscription create(final URL url)
277 {
278 return new Subscription(url);
279 }
280
281 public static Subscription create(final String urlString) throws IOException
282 {
283 try
284 {
285 return new Subscription(new URL(urlString));
286 }
287 catch (final IOException e)
288 {
289 Log.d(TAG, "Creation failed for: '" + urlString + "'");
290 throw e;
291 }
292 }
293
294 /**
295 * Creates a {@code USER} subscription.
296 *
297 * @param title
298 * @return the subscription
299 * @see Subscription.Type
300 */
301 public static Subscription createUserSubscription(final String title)
302 {
303 final Subscription sub = new Subscription();
304 sub.meta.put(KEY_TITLE, title);
305 return sub;
306 }
307
308 public boolean isEnabled()
309 {
310 return "true".equals(this.getMeta(KEY_ENABLED));
311 }
312
313 public void setEnabled(boolean enable)
314 {
315 this.putMeta(KEY_ENABLED, Boolean.toString(enable));
316 }
317
318 public void copyFilters(Collection<String> filters)
319 {
320 if (filters != null)
321 {
322 filters.addAll(this.filters);
323 }
324 }
325
326 public void clearFilters()
327 {
328 this.filters.clear();
329 }
330
331 /**
332 * @return an internal management ID
333 */
334 public String getId()
335 {
336 return getId(this);
337 }
338
339 public static String getId(final Subscription subscription)
340 {
341 switch (subscription.type)
342 {
343 case DOWNLOADABLE:
344 return "url:" + subscription.url.toString();
345 case USER:
346 return "user:" + subscription.getMeta(KEY_TITLE);
347 }
348 return "";
349 }
350
351 private static String byteArrayToHexString(final byte[] array)
352 {
353 final StringBuilder sb = new StringBuilder(array.length * 2);
354 for (final byte b : array)
355 {
356 final int value = b & 255;
357 if (value < 16)
358 {
359 sb.append('0');
360 }
361 sb.append(Integer.toHexString(value));
362 }
363 return sb.toString();
364 }
365
366 private static String createMetaDataHash(final HashMap<String, String> meta) t hrows IOException
367 {
368 final ArrayList<String> keyValues = new ArrayList<>();
369 for (final Entry<String, String> e : meta.entrySet())
370 {
371 if (!KEY_META_HASH.equals(e.getKey()))
372 {
373 keyValues.add(e.getKey() + ":" + e.getValue());
374 }
375 }
376 return createFilterHash(keyValues);
377 }
378
379 private static String createFilterHash(List<String> filters) throws IOExceptio n
380 {
381 try
382 {
383 final MessageDigest md5 = MessageDigest.getInstance("MD5");
384 Collections.sort(filters);
385 for (final String filter : filters)
386 {
387 md5.update(filter.getBytes(StandardCharsets.UTF_8));
388 }
389 return byteArrayToHexString(md5.digest());
390 }
391 catch (final NoSuchAlgorithmException e)
392 {
393 throw new IOException("MD5 is unavailable: " + e.getMessage(), e);
394 }
395 }
396
397 public void serializeMetaData(final File metaFile) throws IOException
398 {
399 this.putMeta(KEY_META_HASH, createMetaDataHash(this.meta));
400 try (final DataOutputStream metaOut = new DataOutputStream(new BufferedOutpu tStream(
401 new GZIPOutputStream(new FileOutputStream(metaFile)))))
402 {
403 metaOut.writeUTF(this.url != null ? this.url.toString() : "");
404 metaOut.writeInt(this.meta.size());
405 for (final Entry<String, String> e : this.meta.entrySet())
406 {
407 metaOut.writeUTF(e.getKey());
408 metaOut.writeUTF(e.getValue());
409 }
410 }
411 }
412
413 public void serializeFilters(final File filtersFile) throws IOException
414 {
415 try (final DataOutputStream filtersOut = new DataOutputStream(new BufferedOu tputStream(
416 new GZIPOutputStream(new FileOutputStream(filtersFile)))))
417 {
418 filtersOut.writeInt(this.filters.size());
419 filtersOut.writeUTF(createFilterHash(new ArrayList<>(this.filters)));
420 for (final String s : this.filters)
421 {
422 final byte[] b = s.getBytes(StandardCharsets.UTF_8);
423 filtersOut.writeInt(b.length);
424 filtersOut.write(b);
425 }
426 }
427 }
428
429 public void serializeSubscription(final File metaFile, final File filtersFile) throws IOException
430 {
431 this.serializeMetaData(metaFile);
432 this.serializeFilters(filtersFile);
433 }
434
435 public static Subscription deserializeSubscription(final File metaFile)
436 {
437 Subscription sub = null;
438 try (final DataInputStream in = new DataInputStream(new BufferedInputStream( new GZIPInputStream(
439 new FileInputStream(metaFile)))))
440 {
441 final String urlString = in.readUTF();
442 sub = new Subscription(!TextUtils.isEmpty(urlString) ? new URL(urlString) : null);
443 sub.metaDataValid = false;
444 final int numMetaEntries = in.readInt();
445 for (int i = 0; i < numMetaEntries; i++)
446 {
447 final String key = in.readUTF();
448 final String value = in.readUTF();
449 sub.meta.put(key, value);
450 }
451 sub.metaDataValid = createMetaDataHash(sub.meta).equals(sub.getMeta(KEY_ME TA_HASH));
452 }
453 catch (Throwable t)
454 {
455 // We catch Throwable here in order to return whatever we could retrieve f rom the meta file
456 }
457 return sub;
458 }
459
460 public void deserializeFilters(final File filtersFile)
461 {
462 this.clearFilters();
463 this.filtersValid = false;
464 try (final DataInputStream in = new DataInputStream(new BufferedInputStream( new GZIPInputStream(
465 new FileInputStream(filtersFile)))))
466 {
467 final int numFilters = in.readInt();
468 final String filtersHash = in.readUTF();
469 for (int i = 0; i < numFilters; i++)
470 {
471 final int length = in.readInt();
472 final byte[] b = new byte[length];
473 in.readFully(b);
474 this.filters.add(new String(b, StandardCharsets.UTF_8));
475 }
476 this.filtersValid = createFilterHash(new ArrayList<>(this.filters)).equals (
477 filtersHash);
478 Log.d(TAG, "Filters valid: " + this.filtersValid);
479 }
480 catch (Throwable t)
481 {
482 // We catch Throwable here in order to load whatever we could retrieve fro m the filters file
483 }
484 }
485
486 /**
487 * Adds the given string, which should be a single filter to this
488 * subscription.
489 *
490 * @param input
491 */
492 public Subscription parseLine(String input)
493 {
494 final String line = input.trim();
495 if (!line.isEmpty())
496 {
497 if (line.startsWith("!"))
498 {
499 // Meta data
500 final int colon = line.indexOf(':');
501 if (colon > 2)
502 {
503 final String key = line.substring(1, colon).trim().toLowerCase(LOCALE_ EN);
504 final String value = line.substring(colon + 1).trim();
505 if (!key.isEmpty() && !value.isEmpty() && ALLOWED_META_KEYS.contains(k ey))
506 {
507 this.meta.put(key, value);
508 }
509 }
510 }
511 else if (line.startsWith("["))
512 {
513 // currently ignored
514 }
515 else
516 {
517 this.filters.add(line);
518 }
519 }
520 return this;
521 }
522
523 public Subscription parseLines(final List<String> lines)
524 {
525 for (String line : lines)
526 {
527 this.parseLine(line);
528 }
529 return this;
530 }
531
532 public Subscription parseText(final String string)
533 {
534 try (final BufferedReader r = new BufferedReader(new StringReader(string)))
535 {
536 for (String line = r.readLine(); line != null; line = r.readLine())
537 {
538 this.parseLine(line);
539 }
540 }
541 catch (final IOException e)
542 {
543 // ignored ... we're reading from a String
544 }
545 return this;
546 }
547
548 boolean updateSubscription(final int responseCode, final String text,
549 final Map<String, String> httpHeaders, final File metaFile, final File fil tersFile)
550 throws IOException
551 {
552 boolean filtersChanged = false;
553 if (responseCode == 304)
554 {
555 // Not changed, update update timestamp only
556 this.meta.put(KEY_UPDATE_TIMESTAMP, Long.toString(System.currentTimeMillis ()));
557 }
558 else
559 {
560 if (responseCode != 200 || text == null)
561 {
562 // We tried, but we failed
563 this.meta.put(KEY_TRIED_UPDATE_TIMESTAMP, Long.toString(System.currentTi meMillis()));
564 }
565 else
566 {
567 // Update succeeded, update filters
568 filtersChanged = true;
569 this.meta.put(KEY_UPDATE_TIMESTAMP, Long.toString(System.currentTimeMill is()));
570 if (httpHeaders != null)
571 {
572 final String etag = httpHeaders.get("etag");
573 final String lastModified = httpHeaders.get("last-modified");
574
575 if (etag != null)
576 {
577 this.meta.put(KEY_HTTP_ETAG, etag);
578 }
579 else
580 {
581 this.meta.remove(KEY_HTTP_ETAG);
582 }
583
584 if (lastModified != null)
585 {
586 this.meta.put(KEY_HTTP_LAST_MODIFIED, lastModified);
587 }
588 else
589 {
590 this.meta.remove(KEY_HTTP_LAST_MODIFIED);
591 }
592 this.meta.put(KEY_DOWNLOAD_COUNT, Long.toString(this.getDownloadCount( ) + 1));
593
594 this.clearFilters();
595 this.parseText(text);
596 }
597 }
598 }
599
600 this.serializeMetaData(metaFile);
601 if (filtersChanged)
602 {
603 this.serializeFilters(filtersFile);
604 this.clearFilters();
605 }
606
607 return filtersChanged;
608 }
609 }
OLDNEW

Powered by Google App Engine
This is Rietveld