Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/tools/xo_bundle/components/nsMicrosummaryService.js
blob: f51c0cd3eb8b2a5a92220bbf503c9a2188dde306 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
//@line 40 "/builds/moz2_slave/linux_build/build/browser/components/microsummaries/src/nsMicrosummaryService.js"

const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;

const PERMS_FILE    = 0644;
const MODE_WRONLY   = 0x02;
const MODE_CREATE   = 0x08;
const MODE_TRUNCATE = 0x20;

const NS_ERROR_MODULE_DOM = 2152923136;
const NS_ERROR_DOM_BAD_URI = NS_ERROR_MODULE_DOM + 1012;

// How often to check for microsummaries that need updating, in milliseconds.
const CHECK_INTERVAL = 15 * 1000; // 15 seconds
// How often to check for generator updates, in seconds
const GENERATOR_INTERVAL = 7 * 86400; // 1 week

const MICSUM_NS = "http://www.mozilla.org/microsummaries/0.1";
const XSLT_NS = "http://www.w3.org/1999/XSL/Transform";

const ANNO_MICSUM_GEN_URI    = "microsummary/generatorURI";
const ANNO_MICSUM_EXPIRATION = "microsummary/expiration";
const ANNO_STATIC_TITLE      = "bookmarks/staticTitle";
const ANNO_CONTENT_TYPE      = "bookmarks/contentType";

const MAX_SUMMARY_LENGTH = 4096;

Cu.import("resource://gre/modules/XPCOMUtils.jsm");

function MicrosummaryService() {
  this._obs.addObserver(this, "xpcom-shutdown", true);

  Cc["@mozilla.org/preferences-service;1"].
    getService(Ci.nsIPrefService).
    getBranch("browser.microsummary.").
    QueryInterface(Ci.nsIPrefBranch2).
    addObserver("", this, true);

  this._initTimers();
  this._cacheLocalGenerators();
}

MicrosummaryService.prototype = {
  // Bookmarks Service
  __bms: null,
  get _bms() {
    if (!this.__bms)
      this.__bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
                   getService(Ci.nsINavBookmarksService);
    return this.__bms;
  },

  // Annotation Service
  __ans: null,
  get _ans() {
    if (!this.__ans)
      this.__ans = Cc["@mozilla.org/browser/annotation-service;1"].
                   getService(Ci.nsIAnnotationService);
    return this.__ans;
  },
 
  // IO Service
  __ios: null,
  get _ios() {
    if (!this.__ios)
      this.__ios = Cc["@mozilla.org/network/io-service;1"].
                   getService(Ci.nsIIOService);
    return this.__ios;
  },

  // Observer Service
  __obs: null,
  get _obs() {
    if (!this.__obs)
      this.__obs = Cc["@mozilla.org/observer-service;1"].
                   getService(Ci.nsIObserverService);
    return this.__obs;
  },

  /**
   * Make a URI from a spec.
   * @param   spec
   *          The string spec of the URI.
   * @returns An nsIURI object.
   */
  _uri: function MSS__uri(spec) {
    return this._ios.newURI(spec, null, null);
  },

  // Directory Locator
  __dirs: null,
  get _dirs() {
    if (!this.__dirs)
      this.__dirs = Cc["@mozilla.org/file/directory_service;1"].
                    getService(Ci.nsIProperties);
    return this.__dirs;
  },

  // The update interval as specified by the user (defaults to 30 minutes)
  get _updateInterval() {
    var updateInterval =
      getPref("browser.microsummary.updateInterval", 30);
    // the minimum update interval is 1 minute
    return Math.max(updateInterval, 1) * 60 * 1000;
  },

  // A cache of local microsummary generators.  This gets built on startup
  // by the _cacheLocalGenerators() method.
  _localGenerators: {},

  // The timer that periodically checks for microsummaries needing updating.
  _timer: null,

  // XPCOM registration
  classDescription: "Microsummary Service",
  contractID: "@mozilla.org/microsummary/service;1",
  classID: Components.ID("{460a9792-b154-4f26-a922-0f653e2c8f91}"),
  QueryInterface: XPCOMUtils.generateQI([Ci.nsIMicrosummaryService, 
                                         Ci.nsISupportsWeakReference,
                                         Ci.nsIObserver]),

  // nsIObserver
  observe: function MSS_observe(subject, topic, data) {
    switch (topic) {
      case "xpcom-shutdown":
        this._destroy();
        break;
      case "nsPref:changed":
        if (data == "enabled")
          this._initTimers();
        break;
    }
  },

  _initTimers: function MSS__initTimers() {
    if (this._timer)
      this._timer.cancel();

    if (!getPref("browser.microsummary.enabled", true))
      return;

    // Periodically update microsummaries that need updating.
    this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    var callback = {
      _svc: this,
      notify: function(timer) { this._svc._updateMicrosummaries() }
    };
    this._timer.initWithCallback(callback,
                                 CHECK_INTERVAL,
                                 this._timer.TYPE_REPEATING_SLACK);

    // Setup a cross-session timer to periodically check for generator updates.
    var updateManager = Cc["@mozilla.org/updates/timer-manager;1"].
                        getService(Ci.nsIUpdateTimerManager);
    var interval = getPref("browser.microsummary.generatorUpdateInterval",
                           GENERATOR_INTERVAL);
    var updateCallback = {
      _svc: this,
      notify: function(timer) { this._svc._updateGenerators() }
    };
    updateManager.registerTimer("microsummary-generator-update-timer",
                                updateCallback, interval);
  },
  
  _destroy: function MSS__destroy() {
    this._timer.cancel();
    this._timer = null;
  },

  _updateMicrosummaries: function MSS__updateMicrosummaries() {
    var bookmarks = this._getBookmarks();

    var now = Date.now();
    var updateInterval = this._updateInterval;
    for ( var i = 0; i < bookmarks.length; i++ ) {
      var bookmarkID = bookmarks[i];

      // Skip this page if its microsummary hasn't expired yet.
      if (this._ans.itemHasAnnotation(bookmarkID, ANNO_MICSUM_EXPIRATION) &&
          this._ans.getItemAnnotation(bookmarkID, ANNO_MICSUM_EXPIRATION) > now)
        continue;

      // Reset the expiration time immediately, so if the refresh is failing
      // we don't try it every 15 seconds, potentially overloading the server.
      this._setAnnotation(bookmarkID, ANNO_MICSUM_EXPIRATION, now + updateInterval);

      // Try to update the microsummary, but trap errors, so an update
      // that throws doesn't prevent us from updating the rest of them.
      try {
        this.refreshMicrosummary(bookmarkID);
      }
      catch(ex) {
        Cu.reportError(ex);
      }
    }
  },

  _updateGenerators: function MSS__updateGenerators() {
    var generators = this._localGenerators;
    var update = getPref("browser.microsummary.updateGenerators", true);
    if (!generators || !update)
      return;

    for (let uri in generators)
      generators[uri].update();
  },

  _updateMicrosummary: function MSS__updateMicrosummary(bookmarkID, microsummary) {
    var title = this._bms.getItemTitle(bookmarkID);

    // Ensure the user-given title is cached
    if (!this._ans.itemHasAnnotation(bookmarkID, ANNO_STATIC_TITLE))
      this._setAnnotation(bookmarkID, ANNO_STATIC_TITLE, title);

    // A string identifying the bookmark to use when logging the update.
    var bookmarkIdentity = bookmarkID;

    // Update if the microsummary differs from the current title.
    if (!title || title != microsummary.content) {
      this._bms.setItemTitle(bookmarkID, microsummary.content);
      var subject = new LiveTitleNotificationSubject(bookmarkID, microsummary);
      LOG("updated live title for " + bookmarkIdentity +
          " from '" + (title == null ? "<no live title>" : title) +
          "' to '" + microsummary.content + "'");
      this._obs.notifyObservers(subject, "microsummary-livetitle-updated", title);
    }
    else {
      LOG("didn't update live title for " + bookmarkIdentity + "; it hasn't changed");
    }

    // Whether or not the title itself has changed, we still save any changes
    // to the update interval, since the interval represents how long to wait
    // before checking again for updates, and that can vary across updates,
    // even when the title itself hasn't changed.
    this._setAnnotation(bookmarkID, ANNO_MICSUM_EXPIRATION,
                  Date.now() + (microsummary.updateInterval || this._updateInterval));
  },

  /**
   * Load local generators into the cache.
   * 
   */
  _cacheLocalGenerators: function MSS__cacheLocalGenerators() {
    // Load generators from the application directory.
    var appDir = this._dirs.get("MicsumGens", Ci.nsIFile);
    if (appDir.exists())
      this._cacheLocalGeneratorDir(appDir);

    // Load generators from the user's profile.
    var profileDir = this._dirs.get("UsrMicsumGens", Ci.nsIFile);
    if (profileDir.exists())
      this._cacheLocalGeneratorDir(profileDir);
  },

  /**
   * Load local generators from a directory into the cache.
   *
   * @param   dir
   *          nsIFile object pointing to directory containing generator files
   * 
   */
  _cacheLocalGeneratorDir: function MSS__cacheLocalGeneratorDir(dir) {
    var files = dir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
    var file = files.nextFile;

    while (file) {
      // Recursively load generators so support packs containing
      // lots of generators can organize them into multiple directories.
      if (file.isDirectory())
        this._cacheLocalGeneratorDir(file);
      else
        this._cacheLocalGeneratorFile(file);

      file = files.nextFile;
    }
    files.close();
  },

  /**
   * Load a local generator from a file into the cache.
   * 
   * @param   file
   *          nsIFile object pointing to file from which to load generator
   * 
   */
  _cacheLocalGeneratorFile: function MSS__cacheLocalGeneratorFile(file) {
    var uri = this._ios.newFileURI(file);

    var t = this;
    var callback =
      function MSS_cacheLocalGeneratorCallback(resource) {
        try     { t._handleLocalGenerator(resource) }
        finally { resource.destroy() }
      };

    var resource = new MicrosummaryResource(uri);
    resource.load(callback);
  },

  _handleLocalGenerator: function MSS__handleLocalGenerator(resource) {
    if (!resource.isXML)
      throw(resource.uri.spec + " microsummary generator loaded, but not XML");

    var generator = new MicrosummaryGenerator(null, resource.uri);
    generator.initFromXML(resource.content);

    // Add the generator to the local generators cache.
    // XXX Figure out why Firefox crashes on shutdown if we index generators
    // by uri.spec but doesn't crash if we index by uri.spec.split().join().
    //this._localGenerators[generator.uri.spec] = generator;
    this._localGenerators[generator.uri.spec.split().join()] = generator;

    LOG("loaded local microsummary generator\n" +
        "  file: " + generator.localURI.spec + "\n" +
        "    ID: " + generator.uri.spec);
  },

  // nsIMicrosummaryService

  /**
   * Return a microsummary generator for the given URI.
   *
   * @param   generatorURI
   *          the URI of the generator
   */
  getGenerator: function MSS_getGenerator(generatorURI) {
    return this._localGenerators[generatorURI.spec] ||
      new MicrosummaryGenerator(generatorURI);
  },

  /**
   * Install the microsummary generator from the resource at the supplied URI.
   * Callable by content via the addMicrosummaryGenerator() sidebar method.
   *
   * @param   generatorURI
   *          the URI of the resource providing the generator
   *
   */
  addGenerator: function MSS_addGenerator(generatorURI) {
    var t = this;
    var callback =
      function MSS_addGeneratorCallback(resource) {
        try     { t._handleNewGenerator(resource) }
        finally { resource.destroy() }
      };

    var resource = new MicrosummaryResource(generatorURI);
    resource.load(callback);
  },

  _handleNewGenerator: function MSS__handleNewGenerator(resource) {
    if (!resource.isXML)
      throw(resource.uri.spec + " microsummary generator loaded, but not XML");

    // XXX Make sure it's a valid microsummary generator.

    var rootNode = resource.content.documentElement;

    // Add a reference to the URI from which we got this generator so we have
    // a unique identifier for the generator and also so we can check back later
    // for updates.
    rootNode.setAttribute("uri", "urn:source:" + resource.uri.spec);

    this.installGenerator(resource.content);
  },
 
  /**
   * Install a microsummary generator from the given XML definition.
   *
   * @param   xmlDefinition
   *          an nsIDOMDocument XML document defining the generator
   *
   * @returns the newly-installed nsIMicrosummaryGenerator generator
   *
   */
  installGenerator: function MSS_installGenerator(xmlDefinition) {
    var rootNode = xmlDefinition.getElementsByTagNameNS(MICSUM_NS, "generator")[0];
 
    var generatorID = rootNode.getAttribute("uri");
 
    // The existing cache entry for this generator, if it is already installed.
    var generator = this._localGenerators[generatorID];

    var topic;
    if (generator)
      topic = "microsummary-generator-updated";
    else {
      // This generator is not already installed.  Save it as a new file.
      topic = "microsummary-generator-installed";
      var generatorName = rootNode.getAttribute("name");
      var fileName = sanitizeName(generatorName) + ".xml";
      var file = this._dirs.get("UsrMicsumGens", Ci.nsIFile);
      file.append(fileName);
      file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE);
      generator = new MicrosummaryGenerator(null, this._ios.newFileURI(file));
      this._localGenerators[generatorID] = generator;
    }
 
    // Initialize (or reinitialize) the generator from its XML definition,
    // the save the definition to the generator's file.
    generator.initFromXML(xmlDefinition);
    generator.saveXMLToFile(xmlDefinition);

    LOG("installed generator " + generatorID);

    this._obs.notifyObservers(generator, topic, null);

    return generator;
  },

  /**
   * Get the set of microsummaries available for a given page.  The set
   * might change after this method returns, since this method will trigger
   * an asynchronous load of the page in question (if it isn't already loaded)
   * to see if it references any page-specific microsummaries.
   *
   * If the caller passes a bookmark ID, and one of the microsummaries
   * is the current one for the bookmark, this method will retrieve content
   * from the datastore for that microsummary, which is useful when callers
   * want to display a list of microsummaries for a page that isn't loaded,
   * and they want to display the actual content of the selected microsummary
   * immediately (rather than after the content is asynchronously loaded).
   *
   * @param   pageURI
   *          the URI of the page for which to retrieve available microsummaries
   *
   * @param   bookmarkID (optional)
   *          the ID of the bookmark for which this method is being called
   *
   * @returns an nsIMicrosummarySet of nsIMicrosummaries for the given page
   *
   */
  getMicrosummaries: function MSS_getMicrosummaries(pageURI, bookmarkID) {
    var microsummaries = new MicrosummarySet();

    if (!getPref("browser.microsummary.enabled", true))
      return microsummaries;

    // Get microsummaries defined by local generators.
    for (var genURISpec in this._localGenerators) {
      var generator = this._localGenerators[genURISpec];

      if (generator.appliesToURI(pageURI)) {
        var microsummary = new Microsummary(pageURI, generator);

        // If this is the current microsummary for this bookmark, load the content
        // from the datastore so it shows up immediately in microsummary picking UI.
        if (bookmarkID != -1 && this.isMicrosummary(bookmarkID, microsummary))
          microsummary._content = this._bms.getItemTitle(bookmarkID);

        microsummaries.AppendElement(microsummary);
      }
    }

    // If a bookmark identifier has been provided, list its microsummary
    // synchronously, if any.
    if (bookmarkID != -1 && this.hasMicrosummary(bookmarkID)) {
      var currentMicrosummary = this.getMicrosummary(bookmarkID);
      if (!microsummaries.hasItemForMicrosummary(currentMicrosummary))
        microsummaries.AppendElement(currentMicrosummary);
    }

    // Get microsummaries defined by the page.  If we don't have the page,
    // download it asynchronously, and then finish populating the set.
    var resource = getLoadedMicrosummaryResource(pageURI);
    if (resource) {
      try     { microsummaries.extractFromPage(resource) }
      finally { resource.destroy() }
    }
    else {
      // Load the page with a callback that will add the page's microsummaries
      // to the set once the page has loaded.
      var callback = function MSS_extractFromPageCallback(resource) {
        try     { microsummaries.extractFromPage(resource) }
        finally { resource.destroy() }
      };

      try {
        resource = new MicrosummaryResource(pageURI);
        resource.load(callback);
      }
      catch(e) {
        // We don't have to do anything special if the call fails besides
        // destroying the Resource object.  We can just return the list
        // of microsummaries without including page-defined microsummaries.
        if (resource)
          resource.destroy();
        LOG("error downloading page to extract its microsummaries: " + e);
      }
    }

    return microsummaries;
  },

  /**
   * Change all occurrences of a specific value in a given field to a new value.
   *
   * @param   fieldName
   *          the name of the field whose values should be changed
   * @param   oldValue
   *          the value that should be changed
   * @param   newValue
   *          the value to which it should be changed
   *
   */
  _changeField: function MSS__changeField(fieldName, oldValue, newValue) {
    var bookmarks = this._getBookmarks();

    for ( var i = 0; i < bookmarks.length; i++ ) {
      var bookmarkID = bookmarks[i];

      if (this._ans.itemHasAnnotation(bookmarkID, fieldName) &&
          this._ans.getItemAnnotation(bookmarkID, fieldName) == oldValue)
        this._setAnnotation(bookmarkID, fieldName, newValue);
    }
  },

  /**
   * Get the set of bookmarks with microsummaries.
   *
   * This is the internal version of this method, which is not accessible
   * via XPCOM but is more performant; inside this component, use this version.
   * Outside the component, use getBookmarks (no underscore prefix) instead.
   *
   * @returns an array of place: uris representing bookmarks items
   *
   */
  _getBookmarks: function MSS__getBookmarks() {
    var bookmarks;

    // This try/catch block is a temporary workaround for bug 336194.
    try {
      bookmarks = this._ans.getItemsWithAnnotation(ANNO_MICSUM_GEN_URI, {});
    }
    catch(e) {
      bookmarks = [];
    }

    return bookmarks;
  },

  _setAnnotation: function MSS__setAnnotation(aBookmarkId, aFieldName, aFieldValue) {
    this._ans.setItemAnnotation(aBookmarkId,
                                aFieldName,
                                aFieldValue,
                                0,
                                this._ans.EXPIRE_NEVER);
  },

  /**
   * Get the set of bookmarks with microsummaries.
   *
   * This is the external version of this method and is accessible via XPCOM.
   * Use it outside this component. Inside the component, use _getBookmarks
   * (with underscore prefix) instead for performance.
   *
   * @returns an nsISimpleEnumerator enumeration of bookmark IDs
   *
   */
  getBookmarks: function MSS_getBookmarks() {
    return new ArrayEnumerator(this._getBookmarks());
  },

  /**
   * Get the current microsummary for the given bookmark.
   *
   * @param   bookmarkID
   *          the bookmark for which to get the current microsummary
   *
   * @returns the current microsummary for the bookmark, or null
   *          if the bookmark does not have a current microsummary
   *
   */
  getMicrosummary: function MSS_getMicrosummary(bookmarkID) {
    if (!this.hasMicrosummary(bookmarkID))
      return null;

    var pageURI = this._bms.getBookmarkURI(bookmarkID);
    var generatorURI = this._uri(this._ans.getItemAnnotation(bookmarkID,
                                                             ANNO_MICSUM_GEN_URI));
    var generator = this.getGenerator(generatorURI);

    return new Microsummary(pageURI, generator);
  },

  /**
   * Get a microsummary for a given page URI and generator URI.
   *
   * @param   pageURI
   *          the URI of the page to be summarized
   *
   * @param   generatorURI
   *          the URI of the microsummary generator
   *
   * @returns an nsIMicrosummary for the given page and generator URIs.
   *
   */
  createMicrosummary: function MSS_createMicrosummary(pageURI, generatorURI) {
    var generator = this.getGenerator(generatorURI);
    return new Microsummary(pageURI, generator);
  },

  /**
   * Set the current microsummary for the given bookmark.
   *
   * @param   bookmarkID
   *          the bookmark for which to set the current microsummary
   *
   * @param   microsummary
   *          the microsummary to set as the current one
   *
   */
  setMicrosummary: function MSS_setMicrosummary(bookmarkID, microsummary) {
    this._setAnnotation(bookmarkID, ANNO_MICSUM_GEN_URI, microsummary.generator.uri.spec);

    if (microsummary.content)
      this._updateMicrosummary(bookmarkID, microsummary);
    else
      this.refreshMicrosummary(bookmarkID);
  },

  /**
   * Remove the current microsummary for the given bookmark.
   *
   * @param   bookmarkID
   *          the bookmark for which to remove the current microsummary
   *
   */
  removeMicrosummary: function MSS_removeMicrosummary(bookmarkID) {
    // Restore the user's title
    if (this._ans.itemHasAnnotation(bookmarkID, ANNO_STATIC_TITLE))
      this._bms.setItemTitle(bookmarkID, this._ans.getItemAnnotation(bookmarkID, ANNO_STATIC_TITLE));

    var fields = [ANNO_MICSUM_GEN_URI,
                  ANNO_MICSUM_EXPIRATION,
                  ANNO_STATIC_TITLE,
                  ANNO_CONTENT_TYPE];

    for (let i = 0; i < fields.length; i++) {
      var field = fields[i];
      if (this._ans.itemHasAnnotation(bookmarkID, field))
        this._ans.removeItemAnnotation(bookmarkID, field);
    }
  },

  /**
   * Whether or not the given bookmark has a current microsummary.
   *
   * @param   bookmarkID
   *          the bookmark for which to set the current microsummary
   *
   * @returns a boolean representing whether or not the given bookmark
   *          has a current microsummary
   *
   */
  hasMicrosummary: function MSS_hasMicrosummary(bookmarkID) {
    return this._ans.itemHasAnnotation(bookmarkID, ANNO_MICSUM_GEN_URI);
  },

  /**
   * Whether or not the given microsummary is the current microsummary
   * for the given bookmark.
   *
   * @param   bookmarkID
   *          the bookmark to check
   *
   * @param   microsummary
   *          the microsummary to check
   *
   * @returns whether or not the microsummary is the current one
   *          for the bookmark
   *
   */
  isMicrosummary: function MSS_isMicrosummary(aBookmarkID, aMicrosummary) {
    if (!aMicrosummary || !aBookmarkID)
      throw Cr.NS_ERROR_INVALID_ARG;

    if (this.hasMicrosummary(aBookmarkID)) {
      currentMicrosummarry = this.getMicrosummary(aBookmarkID);
      if (aMicrosummary.equals(currentMicrosummarry))
        return true;
    }
    return false
  },

  /**
   * Refresh a microsummary, updating its value in the datastore and UI.
   * If this method can refresh the microsummary instantly, it will.
   * Otherwise, it'll asynchronously download the necessary information
   * (the generator and/or page) before refreshing the microsummary.
   *
   * Callers should check the "content" property of the returned microsummary
   * object to distinguish between sync and async refreshes.  If its value
   * is "null", then it's an async refresh, and the caller should register
   * itself as an nsIMicrosummaryObserver via nsIMicrosummary.addObserver()
   * to find out when the refresh completes.
   *
   * @param   bookmarkID
   *          the bookmark whose microsummary is being refreshed
   *
   * @returns the microsummary being refreshed
   *
   */
  refreshMicrosummary: function MSS_refreshMicrosummary(bookmarkID) {
    if (!this.hasMicrosummary(bookmarkID))
      throw "bookmark " + bookmarkID + " does not have a microsummary";

    var pageURI = this._bms.getBookmarkURI(bookmarkID);
    if (!pageURI)
      throw("can't get URL for bookmark with ID " + bookmarkID);
    var generatorURI = this._uri(this._ans.getItemAnnotation(bookmarkID,
                                                             ANNO_MICSUM_GEN_URI));

    var generator = this._localGenerators[generatorURI.spec] ||
                    new MicrosummaryGenerator(generatorURI);

    var microsummary = new Microsummary(pageURI, generator);

    // A microsummary observer that calls the microsummary service
    // to update the datastore when the microsummary finishes loading.
    var observer = {
      _svc: this,
      _bookmarkID: bookmarkID,
      onContentLoaded: function MSS_observer_onContentLoaded(microsummary) {
        try {
          this._svc._updateMicrosummary(this._bookmarkID, microsummary);
        }
        finally {
          this._svc = null;
          this._bookmarkID = null;
          microsummary.removeObserver(this);
        }
      },

      onError: function MSS_observer_onError(microsummary) {
        if (microsummary.needsRemoval)
          this._svc.removeMicrosummary(this._bookmarkID);
      }
    };

    // Register the observer with the microsummary and trigger the microsummary
    // to update itself.
    microsummary.addObserver(observer);
    microsummary.update();
    
    return microsummary;
  }
};





function LiveTitleNotificationSubject(bookmarkID, microsummary) {
  this.bookmarkID = bookmarkID;
  this.microsummary = microsummary;
}

LiveTitleNotificationSubject.prototype = {
  bookmarkID: null,
  microsummary: null,

  // nsISupports
  QueryInterface: XPCOMUtils.generateQI([Ci.nsILiveTitleNotificationSubject]),
};





function Microsummary(aPageURI, aGenerator) {
  this._observers = [];
  this._pageURI = aPageURI || null;
  this._generator = aGenerator || null;
  this._content = null;
  this._pageContent = null;
  this._updateInterval = null;
  this._needsRemoval = false;
}

Microsummary.prototype = {
  // The microsummary service.
  __mss: null,
  get _mss() {
    if (!this.__mss)
      this.__mss = Cc["@mozilla.org/microsummary/service;1"].
                   getService(Ci.nsIMicrosummaryService);
    return this.__mss;
  },

  // IO Service
  __ios: null,
  get _ios() {
    if (!this.__ios)
      this.__ios = Cc["@mozilla.org/network/io-service;1"].
                   getService(Ci.nsIIOService);
    return this.__ios;
  },

  /**
   * Make a URI from a spec.
   * @param   spec
   *          The string spec of the URI.
   * @returns An nsIURI object.
   */
  _uri: function MSS__uri(spec) {
    return this._ios.newURI(spec, null, null);
  },

  // nsISupports
  QueryInterface: XPCOMUtils.generateQI([Ci.nsIMicrosummary]),

  // nsIMicrosummary
  get content() {
    // If we have everything we need to generate the content, generate it.
    if (!this._content &&
        this.generator.loaded &&
        (this.pageContent || !this.generator.needsPageContent)) {
      this._content = this.generator.generateMicrosummary(this.pageContent);
      this._updateInterval = this.generator.calculateUpdateInterval(this.pageContent);
    }

    // Note: we return "null" if the content wasn't already generated and we
    // couldn't retrieve it from the generated title annotation or generate it
    // ourselves.  So callers shouldn't count on getting content; instead,
    // they should call update if the return value of this getter is "null",
    // setting an observer to tell them when content generation is done.
    return this._content;
  },

  get generator()            { return this._generator },
  set generator(newValue)    { return this._generator = newValue },

  get pageURI() { return this._pageURI },

  equals: function(aOther) {
    if (this._generator &&
        this._pageURI.equals(aOther.pageURI) &&
        this._generator.equals(aOther.generator))
      return true;

    return false;
  },

  get pageContent() {
    if (!this._pageContent) {
      // If the page is currently loaded into a browser window, use that.
      var resource = getLoadedMicrosummaryResource(this.pageURI);
      if (resource) {
        this._pageContent = resource.content;
        resource.destroy();
      }
    }

    return this._pageContent;
  },
  set pageContent(newValue) { return this._pageContent = newValue },

  get updateInterval()         { return this._updateInterval; },
  set updateInterval(newValue) { return this._updateInterval = newValue; },

  get needsRemoval() { return this._needsRemoval; },

  // nsIMicrosummary

  addObserver: function MS_addObserver(observer) {
    // Register the observer, but only if it isn't already registered,
    // so that we don't call the same observer twice for any given change.
    if (this._observers.indexOf(observer) == -1)
      this._observers.push(observer);
  },
  
  removeObserver: function MS_removeObserver(observer) {
    //NS_ASSERT(this._observers.indexOf(observer) != -1,
    //          "can't remove microsummary observer " + observer + ": not registered");
  
    //this._observers =
    //  this._observers.filter(function(i) { observer != i });
    if (this._observers.indexOf(observer) != -1)
      this._observers.splice(this._observers.indexOf(observer), 1);
  },

  /**
   * Regenerates the microsummary, asynchronously downloading its generator
   * and content as needed.
   *
   */
  update: function MS_update() {
    LOG("microsummary.update called for page:\n  " + this.pageURI.spec +
        "\nwith generator:\n  " + this.generator.uri.spec);

    var t = this;

    // We use a common error callback here to flag this microsummary for removal
    // if either the generator or page content have gone permanently missing.
    var errorCallback = function MS_errorCallback(resource) {
      if (resource.status == 410) {
        t._needsRemoval = true;
        LOG("server indicated " + resource.uri.spec + " is gone. flagging for removal");
      }

      resource.destroy();

      for (let i = 0; i < t._observers.length; i++)
        t._observers[i].onError(t);
    };

    // If we don't have the generator, download it now.  After it downloads,
    // we'll re-call this method to continue updating the microsummary.
    if (!this.generator.loaded) {
      // If this generator is identified by a URN, then it's a local generator
      // that should have been cached on application start, so it's missing.
      if (this.generator.uri.scheme == "urn") {
        // If it was installed via nsSidebar::addMicrosummaryGenerator (i.e. it
        // has a URN that identifies the source URL from which we installed it),
        // try to reinstall it (once).
        if (/^source:/.test(this.generator.uri.path)) {
          this._reinstallMissingGenerator();
          return;
        }
        else
          throw "missing local generator: " + this.generator.uri.spec;
      }

      LOG("generator not yet loaded; downloading it");
      var generatorCallback =
        function MS_generatorCallback(resource) {
          try     { t._handleGeneratorLoad(resource) }
          finally { resource.destroy() }
        };
      var resource = new MicrosummaryResource(this.generator.uri);
      resource.load(generatorCallback, errorCallback);
      return;
    }

    // If we need the page content, and we don't have it, download it now.
    // Afterwards we'll re-call this method to continue updating the microsummary.
    if (this.generator.needsPageContent && !this.pageContent) {
      LOG("page content not yet loaded; downloading it");
      var pageCallback =
        function MS_pageCallback(resource) {
          try     { t._handlePageLoad(resource) }
          finally { resource.destroy() }
        };
      var resource = new MicrosummaryResource(this.pageURI);
      resource.load(pageCallback, errorCallback);
      return;
    }

    LOG("generator (and page, if needed) both loaded; generating microsummary");

    // Now that we have both the generator and (if needed) the page content,
    // generate the microsummary, then let the observers know about it.
    this._content = this.generator.generateMicrosummary(this.pageContent);
    this._updateInterval = this.generator.calculateUpdateInterval(this.pageContent);
    this.pageContent = null;
    for ( var i = 0; i < this._observers.length; i++ )
      this._observers[i].onContentLoaded(this);

    LOG("generated microsummary: " + this.content);
  },

  _handleGeneratorLoad: function MS__handleGeneratorLoad(resource) {
    LOG(this.generator.uri.spec + " microsummary generator downloaded");
    if (resource.isXML)
      this.generator.initFromXML(resource.content);
    else if (resource.contentType == "text/plain")
      this.generator.initFromText(resource.content);
    else if (resource.contentType == "text/html")
      this.generator.initFromText(resource.content.body.textContent);
    else
      throw("generator is neither XML nor plain text");

    // Only trigger a [content] update if we were able to init the generator. 
    if (this.generator.loaded)
      this.update();
  },

  _handlePageLoad: function MS__handlePageLoad(resource) {
    if (!resource.isXML && resource.contentType != "text/html")
      throw("page is neither HTML nor XML");

    this.pageContent = resource.content;
    this.update();
  },

  /**
   * Try to reinstall a missing local generator that was originally installed
   * from a URL using nsSidebar::addMicrosumaryGenerator.
   *
   */
  _reinstallMissingGenerator: function MS__reinstallMissingGenerator() {
    LOG("attempting to reinstall missing generator " + this.generator.uri.spec);

    var t = this;

    var loadCallback =
      function MS_missingGeneratorLoadCallback(resource) {
        try     { t._handleMissingGeneratorLoad(resource) }
        finally { resource.destroy() }
      };

    var errorCallback =
      function MS_missingGeneratorErrorCallback(resource) {
        try     { t._handleMissingGeneratorError(resource) }
        finally { resource.destroy() }
      };

    try {
      // Extract the URI from which the generator was originally installed.
      var sourceURL = this.generator.uri.path.replace(/^source:/, "");
      var sourceURI = this._uri(sourceURL);

      var resource = new MicrosummaryResource(sourceURI);
      resource.load(loadCallback, errorCallback);
    }
    catch(ex) {
      Cu.reportError(ex);
      this._handleMissingGeneratorError();
    }
  },

  /**
   * Handle a load event for a missing local generator by trying to reinstall
   * the generator.  If this fails, call _handleMissingGeneratorError to unset
   * microsummaries for bookmarks using this generator so we don't repeatedly
   * try to reinstall the generator, creating too much traffic to the website
   * from which we downloaded it.
   *
   * @param resource
   *        the nsIMicrosummaryResource representing the downloaded generator
   *
   */
  _handleMissingGeneratorLoad: function MS__handleMissingGeneratorLoad(resource) {
    try {
      // Make sure the generator is XML, since local generators have to be.
      if (!resource.isXML)
        throw("downloaded, but not XML " + this.generator.uri.spec);

      // Store the generator's ID in its XML definition.
      var generatorID = this.generator.uri.spec;
      resource.content.documentElement.setAttribute("uri", generatorID);

      // Reinstall the generator and replace our placeholder generator object
      // with the newly installed generator.
      this.generator = this._mss.installGenerator(resource.content);

      // A reinstalled generator should always be loaded.  But just in case
      // it isn't, throw an error so we don't get into an infinite loop
      // (otherwise this._update would try again to reinstall it).
      if (!this.generator.loaded)
        throw("supposedly installed, but not in cache " + this.generator.uri.spec);
    }
    catch(ex) {
      Cu.reportError(ex);
      this._handleMissingGeneratorError(resource);
      return;
    }
  
    LOG("reinstall succeeded; resuming update " + this.generator.uri.spec);
    this.update();
  },

  /**
   * Handle an error event for a missing local generator load by unsetting
   * the microsummaries for bookmarks using this generator so we don't
   * repeatedly try to reinstall the generator, creating too much traffic
   * to the website from which we downloaded it.
   *
   * @param resource
   *        the nsIMicrosummaryResource representing the downloaded generator
   *
   */
  _handleMissingGeneratorError: function MS__handleMissingGeneratorError(resource) {
    LOG("reinstall failed; removing microsummaries " + this.generator.uri.spec);
    var bookmarks = this._mss.getBookmarks();
    while (bookmarks.hasMoreElements()) {
      var bookmarkID = bookmarks.getNext();
      var microsummary = this._mss.getMicrosummary(bookmarkID);
      if (microsummary.generator.uri.equals(this.generator.uri)) {
        LOG("removing microsummary for " + microsummary.pageURI.spec);
        this._mss.removeMicrosummary(bookmarkID);
      }
    }
  }

};





function MicrosummaryGenerator(aURI, aLocalURI, aName) {
  this._uri = aURI || null;
  this._localURI = aLocalURI || null;
  this._name = aName || null;
  this._loaded = false;
  this._rules = [];
  this._template = null;
  this._content = null;
}

MicrosummaryGenerator.prototype = {

  // IO Service
  __ios: null,
  get _ios() {
    if (!this.__ios)
      this.__ios = Cc["@mozilla.org/network/io-service;1"].
                   getService(Ci.nsIIOService);
    return this.__ios;
  },

  // nsISupports
  QueryInterface: XPCOMUtils.generateQI([Ci.nsIMicrosummaryGenerator]),

  // nsIMicrosummaryGenerator

  // Normally this is just the URL from which we download the generator,
  // but for generators stored in the app or profile generators directory
  // it's the value of the generator tag's "uri" attribute (or its local URI
  // should the "uri" attribute be missing).
  get uri() { return this._uri || this.localURI },

  // For generators bundled with the browser or installed by the user,
  // the local URI is the URI of the local file containing the generator XML.
  get localURI() { return this._localURI },
  get name() { return this._name },
  get loaded() { return this._loaded },

  equals: function(aOther) {
    // XXX: could the uri attribute for an exposed generator ever be null?
    return aOther.uri.equals(this.uri);
  },

  /**
   * Determines whether or not the generator applies to a given URI.
   * By default, the generator does not apply to any URI.  In order for it
   * to apply to a URI, the URI must match one or more of the generator's
   * "include" rules and not match any of the generator's "exclude" rules.
   *
   * @param   uri
   *          the URI to test to see if this generator applies to it
   *
   * @returns boolean
   *          whether or not the generator applies to the given URI
   *
   */
  appliesToURI: function(uri) {
    var applies = false;

    for ( var i = 0 ; i < this._rules.length ; i++ ) {
      var rule = this._rules[i];

      switch (rule.type) {
      case "include":
        if (rule.regexp.test(uri.spec))
          applies = true;
        break;
      case "exclude":
        if (rule.regexp.test(uri.spec))
          return false;
        break;
      }
    }

    return applies;
  },

  get needsPageContent() {
    if (this._template)
      return true;
    if (this._content)
      return false;

    throw("needsPageContent called on uninitialized microsummary generator");
  },

  /**
   * Initializes a generator from text content.  Generators initialized
   * from text content merely return that content when their generate() method
   * gets called.
   *
   * @param   text
   *          the text content
   */
  initFromText: function(text) {
    this._content = text;
    this._loaded = true;
  },

  /**
   * Initializes a generator from an XML description of it.
   * 
   * @param   xmlDocument
   *          An XMLDocument object describing a microsummary generator.
   *
   */
  initFromXML: function(xmlDocument) {
    // XXX Make sure the argument is a valid generator XML document.

    // XXX I would have wanted to retrieve the info from the XML via E4X,
    // but we'll need to pass the XSLT transform sheet to the XSLT processor,
    // and the processor can't deal with an E4X-wrapped template node.

    // XXX Right now the code retrieves the first "generator" element
    // in the microsummaries namespace, regardless of whether or not
    // it's the root element.  Should it matter?
    var generatorNode = xmlDocument.getElementsByTagNameNS(MICSUM_NS, "generator")[0];
    if (!generatorNode)
      throw Cr.NS_ERROR_FAILURE;

    this._name = generatorNode.getAttribute("name");

    // We have to retrieve the URI from local generators via the "uri" attribute
    // of its generator tag.
    if (this.localURI && generatorNode.hasAttribute("uri"))
      this._uri = this._ios.newURI(generatorNode.getAttribute("uri"), null, null);

    function getFirstChildByTagName(tagName, parentNode, namespace) {
      var nodeList = parentNode.getElementsByTagNameNS(namespace, tagName);
      for (var i = 0; i < nodeList.length; i++) {
        // Make sure that the node is a direct descendent of the generator node
        if (nodeList[i].parentNode == parentNode)
          return nodeList[i];
      }
      return null;
    }

    // Slurp the include/exclude rules that determine the pages to which
    // this generator applies.  Order is important, so we add the rules
    // in the order in which they appear in the XML.
    this._rules.splice(0);
    var pages = getFirstChildByTagName("pages", generatorNode, MICSUM_NS);
    if (pages) {
      // XXX Make sure the pages tag exists.
      for ( var i = 0; i < pages.childNodes.length ; i++ ) {
        var node = pages.childNodes[i];
        if (node.nodeType != node.ELEMENT_NODE ||
            node.namespaceURI != MICSUM_NS ||
            (node.nodeName != "include" && node.nodeName != "exclude"))
          continue;
        var urlRegexp = node.textContent.replace(/^\s+|\s+$/g, "");
        this._rules.push({ type: node.nodeName, regexp: new RegExp(urlRegexp) });
      }
    }

    // allow the generators to set individual update values (even varying
    // depending on certain XPath expressions)
    var update = getFirstChildByTagName("update", generatorNode, MICSUM_NS);
    if (update) {
      function _parseInterval(string) {
        // convert from minute fractions to milliseconds
        // and ensure a minimum value of 1 minute
        return Math.round(Math.max(parseFloat(string) || 0, 1) * 60 * 1000);
      }

      this._unconditionalUpdateInterval =
        update.hasAttribute("interval") ?
        _parseInterval(update.getAttribute("interval")) : null;

      // collect the <condition expression="XPath Expression" interval="time"/> clauses
      this._updateIntervals = new Array();
      for (i = 0; i < update.childNodes.length; i++) {
        node = update.childNodes[i];
        if (node.nodeType != node.ELEMENT_NODE || node.namespaceURI != MICSUM_NS ||
            node.nodeName != "condition")
          continue;
        if (!node.getAttribute("expression") || !node.getAttribute("interval")) {
          LOG("ignoring incomplete conditional update interval for " + this.uri.spec);
          continue;
        }
        this._updateIntervals.push({
          expression: node.getAttribute("expression"),
          interval: _parseInterval(node.getAttribute("interval"))
        });
      }
    }

    var templateNode = getFirstChildByTagName("template", generatorNode, MICSUM_NS);
    if (templateNode) {
      this._template = getFirstChildByTagName("transform", templateNode, XSLT_NS) ||
                       getFirstChildByTagName("stylesheet", templateNode, XSLT_NS);
    }
    // XXX Make sure the template is a valid XSL transform sheet.

    this._loaded = true;
  },

  generateMicrosummary: function MSD_generateMicrosummary(pageContent) {

    var content;

    if (this._content)
      content = this._content;
    else if (this._template)
      content = this._processTemplate(pageContent);
    else
      throw("generateMicrosummary called on uninitialized microsummary generator");

    // Clean up the output
    content = content.replace(/^\s+|\s+$/g, "");
    if (content.length > MAX_SUMMARY_LENGTH) 
      content = content.substring(0, MAX_SUMMARY_LENGTH);

    return content;
  },

  calculateUpdateInterval: function MSD_calculateUpdateInterval(doc) {
    if (this._content || !this._updateIntervals || !doc)
      return null;

    for (var i = 0; i < this._updateIntervals.length; i++) {
      try {
        if (doc.evaluate(this._updateIntervals[i].expression, doc, null,
                         Ci.nsIDOMXPathResult.BOOLEAN_TYPE, null).booleanValue)
          return this._updateIntervals[i].interval;
      }
      catch (ex) {
        Cu.reportError(ex);
        // remove the offending conditional update interval
        this._updateIntervals.splice(i--, 1);
      }
    }

    return this._unconditionalUpdateInterval;
  },

  _processTemplate: function MSD__processTemplate(doc) {
    LOG("processing template " + this._template + " against document " + doc);

    // XXX Should we just have one global instance of the processor?
    var processor = Cc["@mozilla.org/document-transformer;1?type=xslt"].
                    createInstance(Ci.nsIXSLTProcessor);

    // Turn off document loading of all kinds (document(), <include>, <import>)
    // for security (otherwise local generators would be able to load local files).
    processor.flags |= Ci.nsIXSLTProcessorPrivate.DISABLE_ALL_LOADS;

    processor.importStylesheet(this._template);
    var fragment = processor.transformToFragment(doc, doc);

    LOG("template processing result: " + fragment.textContent);

    // XXX When we support HTML microsummaries we'll need to do something
    // more sophisticated than just returning the text content of the fragment.
    return fragment.textContent;
  },

  saveXMLToFile: function MSD_saveXMLToFile(xmlDefinition) {
    var file = this.localURI.QueryInterface(Ci.nsIFileURL).file.clone();

    LOG("saving definition to " + file.path);

    // Write the generator XML to the local file.
    var outputStream = Cc["@mozilla.org/network/safe-file-output-stream;1"].
                       createInstance(Ci.nsIFileOutputStream);
    var localFile = file.QueryInterface(Ci.nsILocalFile);
    outputStream.init(localFile, (MODE_WRONLY | MODE_TRUNCATE | MODE_CREATE),
                      PERMS_FILE, 0);
    var serializer = Cc["@mozilla.org/xmlextras/xmlserializer;1"].
                     createInstance(Ci.nsIDOMSerializer);
    serializer.serializeToStream(xmlDefinition, outputStream, null);
    if (outputStream instanceof Ci.nsISafeOutputStream) {
      try       { outputStream.finish() }
      catch (e) { outputStream.close()  }
    }
    else
      outputStream.close();
  },

  update: function MSD_update() {
    // Update this generator if it was downloaded from a remote source and has
    // been modified since we last downloaded it.
    var genURI = this.uri;
    if (genURI && /^urn:source:/i.test(genURI.spec)) {
      let genURL = genURI.spec.replace(/^urn:source:/, "");
      genURI = this._ios.newURI(genURL, null, null);
    }

    // Only continue if we have a valid remote URI
    if (!genURI || !/^https?/.test(genURI.scheme)) {
      LOG("generator did not have valid URI; skipping update: " + genURI.spec);
      return;
    }

    // We use a HEAD request to check if the generator has been modified since
    // the last time we downloaded it. If it has, we move to _preformUpdate() to
    // actually download and save the new generator.
    var t = this;
    var loadCallback = function(resource) {
      if (resource.status != 304)
        t._performUpdate(genURI);
      else
        LOG("generator is already up to date: " + genURI.spec);
      resource.destroy();
    };
    var errorCallback = function(resource) {
      resource.destroy();
    };

    var file = this.localURI.QueryInterface(Ci.nsIFileURL).file.clone();
    var lastmod = new Date(file.lastModifiedTime);
    LOG("updating generator: " + genURI.spec);
    var resource = new MicrosummaryResource(genURI);
    resource.lastMod = lastmod.toUTCString();
    resource.method = "HEAD";
    resource.load(loadCallback, errorCallback);
  },

  _performUpdate: function MSD__performUpdate(uri) {
    var t = this;
    var loadCallback = function(resource) {
      try     { t._handleUpdateLoad(resource) }
      finally { resource.destroy() }
    };
    var errorCallback = function(resource) {
      resource.destroy();
    };

    var resource = new MicrosummaryResource(uri);
    resource.load(loadCallback, errorCallback);
  },

  _handleUpdateLoad: function MSD__handleUpdateLoad(resource) {
    if (!resource.isXML)
      throw("update failed, downloaded resource is not XML: " + this.uri.spec);

    // Preserve the generator's ID.
    // XXX Check for redirects and update the URI if it changes.
    var generatorID = this.uri.spec;
    resource.content.documentElement.setAttribute("uri", generatorID);

    // Reinitialize this generator with the newly downloaded XML and save to disk.
    this.initFromXML(resource.content);
    this.saveXMLToFile(resource.content);

    // Let observers know we've updated this generator
    var obs = Cc["@mozilla.org/observer-service;1"].
              getService(Ci.nsIObserverService);
    obs.notifyObservers(this, "microsummary-generator-updated", null);
  }
};





// Microsummary sets are collections of microsummaries.  They allow callers
// to register themselves as observers of the set, and when any microsummary
// in the set changes, the observers get notified.  Thus a caller can observe
// the set instead of each individual microsummary.

function MicrosummarySet() {
  this._observers = [];
  this._elements = [];
}

MicrosummarySet.prototype = {
  // IO Service
  __ios: null,
  get _ios() {
    if (!this.__ios)
      this.__ios = Cc["@mozilla.org/network/io-service;1"].
                   getService(Ci.nsIIOService);
    return this.__ios;
  },

  QueryInterface: XPCOMUtils.generateQI([Ci.nsIMicrosummarySet,
                                         Ci.nsIMicrosummaryObserver]),

  // nsIMicrosummaryObserver

  onContentLoaded: function MSSet_onContentLoaded(microsummary) {
    for ( var i = 0; i < this._observers.length; i++ )
      this._observers[i].onContentLoaded(microsummary);
  },

  onError: function MSSet_onError(microsummary) {
    for ( var i = 0; i < this._observers.length; i++ )
      this._observers[i].onError(microsummary);
  },

  // nsIMicrosummarySet

  addObserver: function MSSet_addObserver(observer) {
    if (this._observers.length == 0) {
      for ( var i = 0 ; i < this._elements.length ; i++ )
        this._elements[i].addObserver(this);
    }

    // Register the observer, but only if it isn't already registered,
    // so that we don't call the same observer twice for any given change.
    if (this._observers.indexOf(observer) == -1)
      this._observers.push(observer);
  },
  
  removeObserver: function MSSet_removeObserver(observer) {
    //NS_ASSERT(this._observers.indexOf(observer) != -1,
    //          "can't remove microsummary observer " + observer + ": not registered");
  
    //this._observers =
    //  this._observers.filter(function(i) { observer != i });
    if (this._observers.indexOf(observer) != -1)
      this._observers.splice(this._observers.indexOf(observer), 1);
    
    if (this._observers.length == 0) {
      for ( var i = 0 ; i < this._elements.length ; i++ )
        this._elements[i].removeObserver(this);
    }
  },

  extractFromPage: function MSSet_extractFromPage(resource) {
    if (!resource.isXML && resource.contentType != "text/html")
      throw("page is neither HTML nor XML");

    // XXX Handle XML documents, whose microsummaries are specified
    // via processing instructions.

    var links = resource.content.getElementsByTagName("link");
    for ( var i = 0; i < links.length; i++ ) {
      var link = links[i];

      if(!link.hasAttribute("rel"))
        continue;

      var relAttr = link.getAttribute("rel");

      // The attribute's value can be a space-separated list of link types,
      // check to see if "microsummary" is one of them.
      var linkTypes = relAttr.split(/\s+/);
      if (!linkTypes.some( function(v) { return v.toLowerCase() == "microsummary"; }))
        continue;


      // Look for a TITLE attribute to give the generator a nice name in the UI.
      var linkTitle = link.getAttribute("title");


      // Unlike the "href" attribute, the "href" property contains
      // an absolute URI spec, so we use it here to create the URI.
      var generatorURI = this._ios.newURI(link.href,
                                          resource.content.characterSet,
                                          null);

      if (!/^https?$/i.test(generatorURI.scheme)) {
        LOG("can't load generator " + generatorURI.spec + " from page " +
            resource.uri.spec);
        continue;
      }

      var generator = new MicrosummaryGenerator(generatorURI, null, linkTitle);
      var microsummary = new Microsummary(resource.uri, generator);
      if (!this.hasItemForMicrosummary(microsummary))
        this.AppendElement(microsummary);
    }
  },

  /**
   * Determines whether the given microsumary is already represented in the
   * set.
   */
  hasItemForMicrosummary: function MSSet_hasItemForMicrosummary(aMicrosummary) {
    for (var i = 0; i < this._elements.length; i++) {
      if (this._elements[i].equals(aMicrosummary))
        return true;
    }
    return false;
  },

  // XXX Turn this into a complete implementation of nsICollection?
  AppendElement: function MSSet_AppendElement(element) {
    // Query the element to a microsummary.
    // XXX Should we NS_ASSERT if this fails?
    element = element.QueryInterface(Ci.nsIMicrosummary);

    if (this._elements.indexOf(element) == -1) {
      this._elements.push(element);
      element.addObserver(this);
    }

    // Notify observers that an element has been appended.
    for ( var i = 0; i < this._observers.length; i++ )
      this._observers[i].onElementAppended(element);
  },

  Enumerate: function MSSet_Enumerate() {
    return new ArrayEnumerator(this._elements);
  }
};





/**
 * An enumeration of items in a JS array.
 * @constructor
 */
function ArrayEnumerator(aItems) {
  if (aItems) {
    for (var i = 0; i < aItems.length; ++i) {
      if (!aItems[i])
        aItems.splice(i--, 1);
    }
    this._contents = aItems;
  } else {
    this._contents = [];
  }
}

ArrayEnumerator.prototype = {
  QueryInterface: XPCOMUtils.generateQI([Ci.nsISimpleEnumerator]),

  _index: 0,

  hasMoreElements: function() {
    return this._index < this._contents.length;
  },

  getNext: function() {
    return this._contents[this._index++];      
  }
};





/**
 * Outputs aText to the JavaScript console as well as to stdout if the
 * microsummary logging pref (browser.microsummary.log) is set to true.
 * 
 * @param aText
 *        the text to log
 */
function LOG(aText) {
  var f = arguments.callee;
  if (!("_enabled" in f))
    f._enabled = getPref("browser.microsummary.log", false);
  if (f._enabled) {
    dump("*** Microsummaries: " +  aText + "\n");
    var consoleService = Cc["@mozilla.org/consoleservice;1"].
                         getService(Ci.nsIConsoleService);
    consoleService.logStringMessage(aText);
  }
}





/**
 * A resource (page, microsummary, generator, etc.) identifiable by URI.
 * This object abstracts away much of the code for loading resources
 * and parsing their content if they are XML or HTML.
 * 
 * @constructor
 * 
 * @param   uri
 *          the location of the resource
 *
 */
function MicrosummaryResource(uri) {
  // Make sure we're not loading javascript: or data: URLs, which could
  // take advantage of the load to run code with chrome: privileges.
  // XXX Perhaps use nsIScriptSecurityManager.checkLoadURI instead.
  if (!(uri.schemeIs("http") || uri.schemeIs("https") || uri.schemeIs("file")))
    throw NS_ERROR_DOM_BAD_URI;

  this._uri = uri;
  this._content = null;
  this._contentType = null;
  this._isXML = false;
  this.__authFailed = false;
  this._status = null;
  this._method = "GET";
  this._lastMod = null;

  // A function to call when we finish loading/parsing the resource.
  this._loadCallback = null;
  // A function to call if we get an error while loading/parsing the resource.
  this._errorCallback = null;
  // A hidden iframe to parse HTML content.
  this._iframe = null;
}

MicrosummaryResource.prototype = {
  // IO Service
  __ios: null,
  get _ios() {
    if (!this.__ios)
      this.__ios = Cc["@mozilla.org/network/io-service;1"].
                   getService(Ci.nsIIOService);
    return this.__ios;
  },

  get uri() {
    return this._uri;
  },

  get content() {
    return this._content;
  },

  get contentType() {
    return this._contentType;
  },

  get isXML() {
    return this._isXML;
  },

  get status()        { return this._status },
  set status(aStatus) { this._status = aStatus },

  get method()        { return this._method },
  set method(aMethod) { this._method = aMethod },

  get lastMod()     { return this._lastMod },
  set lastMod(aMod) { this._lastMod = aMod },

  // Implement notification callback interfaces so we can suppress UI
  // and abort loads for bad SSL certs and HTTP authorization requests.
  
  // Interfaces this component implements.
  interfaces: [Ci.nsIAuthPromptProvider,
               Ci.nsIAuthPrompt,
               Ci.nsIBadCertListener2,
               Ci.nsISSLErrorListener,
               Ci.nsIPrompt,
               Ci.nsIProgressEventSink,
               Ci.nsIInterfaceRequestor,
               Ci.nsISupports],

  // nsISupports

  QueryInterface: function MSR_QueryInterface(iid) {
    if (!this.interfaces.some( function(v) { return iid.equals(v) } ))
      throw Cr.NS_ERROR_NO_INTERFACE;

    // nsIAuthPrompt and nsIPrompt need separate implementations because
    // their method signatures conflict.  The other interfaces we implement
    // within MicrosummaryResource itself.
    switch(iid) {
    case Ci.nsIAuthPrompt:
      return this.authPrompt;
    case Ci.nsIPrompt:
      return this.prompt;
    default:
      return this;
    }
  },

  // nsIInterfaceRequestor
  
  getInterface: function MSR_getInterface(iid) {
    return this.QueryInterface(iid);
  },

  // nsIBadCertListener2
  // Suppress any certificate errors
  notifyCertProblem: function MSR_certProblem(socketInfo, status, targetSite) {
    return true;
  },

  // nsISSLErrorListener
  // Suppress any ssl errors
  notifySSLError: function MSR_SSLError(socketInfo, error, targetSite) {
    return true;
  },

  
  // Suppress UI and abort loads for files secured by authentication.

  // Auth requests appear to succeed when we cancel them (since the server
  // redirects us to a "you're not authorized" page), so we have to set a flag
  // to let the load handler know to treat the load as a failure.
  get _authFailed()         { return this.__authFailed; },
  set _authFailed(newValue) { return this.__authFailed = newValue },

  // nsIAuthPromptProvider
  
  getAuthPrompt: function(aPromptReason, aIID) {
    this._authFailed = true;
    throw Cr.NS_ERROR_NOT_AVAILABLE;
  },

  // HTTP always requests nsIAuthPromptProvider first, so it never needs
  // nsIAuthPrompt, but not all channels use nsIAuthPromptProvider, so we
  // implement nsIAuthPrompt too.

  // nsIAuthPrompt

  get authPrompt() {
    var resource = this;
    return {
      QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrompt]),
      prompt: function(dialogTitle, text, passwordRealm, savePassword, defaultText, result) {
        resource._authFailed = true;
        return false;
      },
      promptUsernameAndPassword: function(dialogTitle, text, passwordRealm, savePassword, user, pwd) {
        resource._authFailed = true;
        return false;
      },
      promptPassword: function(dialogTitle, text, passwordRealm, savePassword, pwd) {
        resource._authFailed = true;
        return false;
      }
    };
  },

  // nsIPrompt

  get prompt() {
    var resource = this;
    return {
      QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrompt]),
      alert: function(dialogTitle, text) {
        throw Cr.NS_ERROR_NOT_IMPLEMENTED;
      },
      alertCheck: function(dialogTitle, text, checkMessage, checkValue) {
        throw Cr.NS_ERROR_NOT_IMPLEMENTED;
      },
      confirm: function(dialogTitle, text) {
        throw Cr.NS_ERROR_NOT_IMPLEMENTED;
      },
      confirmCheck: function(dialogTitle, text, checkMessage, checkValue) {
        throw Cr.NS_ERROR_NOT_IMPLEMENTED;
      },
      confirmEx: function(dialogTitle, text, buttonFlags, button0Title, button1Title, button2Title, checkMsg, checkValue) {
        throw Cr.NS_ERROR_NOT_IMPLEMENTED;
      },
      prompt: function(dialogTitle, text, value, checkMsg, checkValue) {
        throw Cr.NS_ERROR_NOT_IMPLEMENTED;
      },
      promptPassword: function(dialogTitle, text, password, checkMsg, checkValue) {
        resource._authFailed = true;
        return false;
      },
      promptUsernameAndPassword: function(dialogTitle, text, username, password, checkMsg, checkValue) {
        resource._authFailed = true;
        return false;
      },
      select: function(dialogTitle, text, count, selectList, outSelection) {
        throw Cr.NS_ERROR_NOT_IMPLEMENTED;
      }
    };
  },

  // XXX We implement nsIProgressEventSink because otherwise bug 253127
  // would cause too many extraneous errors to get reported to the console.
  // Fortunately this doesn't screw up XMLHttpRequest, because it ensures
  // that its implementation of nsIProgressEventSink will always get called
  // in addition to whatever notification callbacks we set on the channel.

  // nsIProgressEventSink

  onProgress: function(aRequest, aContext, aProgress, aProgressMax) {},
  onStatus: function(aRequest, aContext, aStatus, aStatusArg) {},

  /**
   * Initialize the resource from an existing DOM document object.
   * 
   * @param   document
   *          a DOM document object
   *
   */
  initFromDocument: function MSR_initFromDocument(document) {
    this._content = document;
    this._contentType = document.contentType;

    // Normally we set this property based on whether or not
    // XMLHttpRequest parsed the content into an XML document object,
    // but since we already have the content, we have to analyze
    // its content type ourselves to see if it is XML.
    this._isXML = (this.contentType == "text/xml" ||
                   this.contentType == "application/xml" ||
                   /^.+\/.+\+xml$/.test(this.contentType));
  },

  /**
   * Destroy references to avoid leak-causing cycles.  Instantiators must call
   * this method on all instances they instantiate once they're done with them.
   *
   */
  destroy: function MSR_destroy() {
    this._uri = null;
    this._content = null;
    this._loadCallback = null;
    this._errorCallback = null;
    this._loadTimer = null;
    this._authFailed = false;
    if (this._iframe) {
      if (this._iframe && this._iframe.parentNode)
        this._iframe.parentNode.removeChild(this._iframe);
      this._iframe = null;
    }
  },

  /**
   * Load the resource.
   * 
   * @param   loadCallback
   *          a function to invoke when the resource finishes loading
   * @param   errorCallback
   *          a function to invoke when an error occurs during the load
   *
   */
  load: function MSR_load(loadCallback, errorCallback) {
    LOG(this.uri.spec + " loading");
  
    this._loadCallback = loadCallback;
    this._errorCallback = errorCallback;

    var request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance();
  
    var loadHandler = {
      _self: this,
      handleEvent: function MSR_loadHandler_handleEvent(event) {
        if (this._self._loadTimer)
          this._self._loadTimer.cancel();

        this._self.status = event.target.status;

        if (this._self._authFailed || this._self.status >= 400) {
          // Technically the request succeeded, but we treat it as a failure,
          // since we won't be able to extract anything relevant from the result.

          // XXX For now HTTP is the only protocol we handle that might fail
          // auth. This message will need to change once we support FTP, which
          // returns 0 for all statuses.
          LOG(this._self.uri.spec + " load failed; HTTP status: " + this._self.status);
          try     { this._self._handleError(event) }
          finally { this._self = null }
        }
        else if (event.target.channel.contentType == "multipart/x-mixed-replace") {
          // Technically the request succeeded, but we treat it as a failure,
          // since we aren't able to handle multipart content.
          LOG(this._self.uri.spec + " load failed; contains multipart content");
          try     { this._self._handleError(event) }
          finally { this._self = null }
        }
        else {
          LOG(this._self.uri.spec + " load succeeded; invoking callback");
          try     { this._self._handleLoad(event) }
          finally { this._self = null }
        }
      }
    };

    var errorHandler = {
      _self: this,
      handleEvent: function MSR_errorHandler_handleEvent(event) {
        if (this._self._loadTimer)
          this._self._loadTimer.cancel();

        LOG(this._self.uri.spec + " load failed");
        try     { this._self._handleError(event) }
        finally { this._self = null }
      }
    };

    // cancel loads that take too long
    // timeout specified in seconds at browser.microsummary.requestTimeout,
    // or 300 seconds (five minutes)
    var timeout = getPref("browser.microsummary.requestTimeout", 300) * 1000;
    var timerObserver = {
      _self: this,
      observe: function MSR_timerObserver_observe() {
        LOG("timeout loading microsummary resource " + this._self.uri.spec + ", aborting request");
        request.abort();
        try     { this._self.destroy() }
        finally { this._self = null }
      }
    };
    this._loadTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    this._loadTimer.init(timerObserver, timeout, Ci.nsITimer.TYPE_ONE_SHOT);

    request = request.QueryInterface(Ci.nsIDOMEventTarget);
    request.addEventListener("load", loadHandler, false);
    request.addEventListener("error", errorHandler, false);
    
    request = request.QueryInterface(Ci.nsIXMLHttpRequest);
    request.open(this.method, this.uri.spec, true);
    request.setRequestHeader("X-Moz", "microsummary");
    if (this.lastMod)
      request.setRequestHeader("If-Modified-Since", this.lastMod);

    // Register ourselves as a listener for notification callbacks so we
    // can handle authorization requests and SSL issues like cert mismatches.
    // XMLHttpRequest will handle the notifications we don't handle.
    request.channel.notificationCallbacks = this;

    // If this is a bookmarked resource, and the bookmarks service recorded
    // its charset in the bookmarks datastore the last time the user visited it,
    // then specify the charset in the channel so XMLHttpRequest loads
    // the resource correctly.
    try {
      var resolver = Cc["@mozilla.org/embeddor.implemented/bookmark-charset-resolver;1"].
                     getService(Ci.nsICharsetResolver);
      if (resolver) {
        var charset = resolver.requestCharset(null, request.channel, {}, {});
        if (charset != "")
          request.channel.contentCharset = charset;
      }
    }
    catch(ex) {}

    request.send(null);
  },

  _handleLoad: function MSR__handleLoad(event) {
    var request = event.target;

    if (request.responseXML) {
      this._isXML = true;
      // XXX Figure out the parsererror format and log a specific error.
      if (request.responseXML.documentElement.nodeName == "parsererror") {
        this._handleError(event);
        return;
      }
      this._content = request.responseXML;
      this._contentType = request.channel.contentType;
      this._loadCallback(this);
    }

    else if (request.channel.contentType == "text/html") {
      this._parse(request.responseText);
    }

    else {
      // This catches text/plain as well as any other content types
      // not accounted for by the content type-specific code above.
      this._content = request.responseText;
      this._contentType = request.channel.contentType;
      this._loadCallback(this);
    }
  },

  _handleError: function MSR__handleError(event) {
    // Call the error callback, then destroy ourselves to prevent memory leaks.
    try     { if (this._errorCallback) this._errorCallback(this) } 
    finally { this.destroy() }
  },

  /**
   * Parse a string of HTML text.  Used by _load() when it retrieves HTML.
   * We do this via hidden XUL iframes, which according to bz is the best way
   * to do it currently, since bug 102699 is hard to fix.
   * 
   * @param   htmlText
   *          a string containing the HTML content
   *
   */
  _parse: function MSR__parse(htmlText) {
    // Find a window to stick our hidden iframe into.
    var windowMediator = Cc['@mozilla.org/appshell/window-mediator;1'].
                         getService(Ci.nsIWindowMediator);
    var window = windowMediator.getMostRecentWindow("navigator:browser");
    // XXX We can use other windows, too, so perhaps we should try to get
    // some other window if there's no browser window open.  Perhaps we should
    // even prefer other windows, since there's less chance of any browser
    // window machinery like throbbers treating our load like one initiated
    // by the user.
    if (!window) {
      this._handleError(event);
      return;
    }
    var document = window.document;
    var rootElement = document.documentElement;
  
    // Create an iframe, make it hidden, and secure it against untrusted content.
    this._iframe = document.createElement('iframe');
    this._iframe.setAttribute("collapsed", true);
    this._iframe.setAttribute("type", "content");
  
    // Insert the iframe into the window, creating the doc shell.
    rootElement.appendChild(this._iframe);

    // When we insert the iframe into the window, it immediately starts loading
    // about:blank, which we don't need and could even hurt us (for example
    // by triggering bugs like bug 344305), so cancel that load.
    var webNav = this._iframe.docShell.QueryInterface(Ci.nsIWebNavigation);
    webNav.stop(Ci.nsIWebNavigation.STOP_NETWORK);

    // Turn off JavaScript and auth dialogs for security and other things
    // to reduce network load.
    // XXX We should also turn off CSS.
    this._iframe.docShell.allowJavascript = false;
    this._iframe.docShell.allowAuth = false;
    this._iframe.docShell.allowPlugins = false;
    this._iframe.docShell.allowMetaRedirects = false;
    this._iframe.docShell.allowSubframes = false;
    this._iframe.docShell.allowImages = false;
  
    var parseHandler = {
      _self: this,
      handleEvent: function MSR_parseHandler_handleEvent(event) {
        event.target.removeEventListener("DOMContentLoaded", this, false);
        try     { this._self._handleParse(event) }
        finally { this._self = null }
      }
    };
 
    // Convert the HTML text into an input stream.
    var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
                    createInstance(Ci.nsIScriptableUnicodeConverter);
    converter.charset = "UTF-8";
    var stream = converter.convertToInputStream(htmlText);

    // Set up a channel to load the input stream.
    var channel = Cc["@mozilla.org/network/input-stream-channel;1"].
                  createInstance(Ci.nsIInputStreamChannel);
    channel.setURI(this._uri);
    channel.contentStream = stream;

    // Load in the background so we don't trigger web progress listeners.
    var request = channel.QueryInterface(Ci.nsIRequest);
    request.loadFlags |= Ci.nsIRequest.LOAD_BACKGROUND;

    // Specify the content type since we're not loading content from a server,
    // so it won't get specified for us, and if we don't specify it ourselves,
    // then Firefox will prompt the user to download content of "unknown type".
    var baseChannel = channel.QueryInterface(Ci.nsIChannel);
    baseChannel.contentType = "text/html";

    // Load as UTF-8, which it'll always be, because XMLHttpRequest converts
    // the text (i.e. XMLHTTPRequest.responseText) from its original charset
    // to UTF-16, then the string input stream component converts it to UTF-8.
    baseChannel.contentCharset = "UTF-8";

    // Register the parse handler as a load event listener and start the load.
    // Listen for "DOMContentLoaded" instead of "load" because background loads
    // don't fire "load" events.
    this._iframe.addEventListener("DOMContentLoaded", parseHandler, true);
    var uriLoader = Cc["@mozilla.org/uriloader;1"].getService(Ci.nsIURILoader);
    uriLoader.openURI(channel, true, this._iframe.docShell);
  },

  /**
   * Handle a load event for the iframe-based parser.
   * 
   * @param   event
   *          the event object representing the load event
   *
   */
  _handleParse: function MSR__handleParse(event) {
    // XXX Make sure the parse was successful?

    this._content = this._iframe.contentDocument;
    this._contentType = this._iframe.contentDocument.contentType;
    this._loadCallback(this);
  }

};

/**
 * Get a resource currently loaded into a browser window.  Checks windows
 * one at a time, starting with the frontmost (a.k.a. most recent) one.
 * 
 * @param   uri
 *          the URI of the resource
 *
 * @returns a Resource object, if the resource is currently loaded
 *          into a browser window; otherwise null
 *
 */
function getLoadedMicrosummaryResource(uri) {
  var mediator = Cc["@mozilla.org/appshell/window-mediator;1"].
                 getService(Ci.nsIWindowMediator);

  // Apparently the Z order enumerator is broken on Linux per bug 156333.
  //var windows = mediator.getZOrderDOMWindowEnumerator("navigator:browser", true);
  var windows = mediator.getEnumerator("navigator:browser");

  while (windows.hasMoreElements()) {
    var win = windows.getNext();
    var tabBrowser = win.document.getElementById("content");
    for ( var i = 0; i < tabBrowser.browsers.length; i++ ) {
      var browser = tabBrowser.browsers[i];
      if (uri.equals(browser.currentURI)) {
        var resource = new MicrosummaryResource(uri);
        resource.initFromDocument(browser.contentDocument);
        return resource;
      }
    }
  }

  return null;
}

/**
 * Get a value from a pref or a default value if the pref doesn't exist.
 *
 * @param   prefName
 * @param   defaultValue
 * @returns the pref's value or the default (if it is missing)
 */
function getPref(prefName, defaultValue) {
  try {
    var prefBranch = Cc["@mozilla.org/preferences-service;1"].
                     getService(Ci.nsIPrefBranch);
    var type = prefBranch.getPrefType(prefName);
    switch (type) {
      case prefBranch.PREF_BOOL:
        return prefBranch.getBoolPref(prefName);
      case prefBranch.PREF_INT:
        return prefBranch.getIntPref(prefName);
    }
  }
  catch (ex) { /* return the default value */ }
  
  return defaultValue;
}


// From http://lxr.mozilla.org/mozilla/source/browser/components/search/nsSearchService.js

/**
 * Removes all characters not in the "chars" string from aName.
 *
 * @returns a sanitized name to be used as a filename, or a random name
 *          if a sanitized name cannot be obtained (if aName contains
 *          no valid characters).
 */
function sanitizeName(aName) {
  const chars = "-abcdefghijklmnopqrstuvwxyz0123456789";
  const maxLength = 60;

  var name = aName.toLowerCase();
  name = name.replace(/ /g, "-");
  //name = name.split("").filter(function (el) {
  //                               return chars.indexOf(el) != -1;
  //                             }).join("");
  var filteredName = "";
  for ( var i = 0 ; i < name.length ; i++ )
    if (chars.indexOf(name[i]) != -1)
      filteredName += name[i];
  name = filteredName;

  if (!name) {
    // Our input had no valid characters - use a random name
    for (var i = 0; i < 8; ++i)
      name += chars.charAt(Math.round(Math.random() * (chars.length - 1)));
  }

  if (name.length > maxLength)
    name = name.substring(0, maxLength);

  return name;
}

function NSGetModule(compMgr, fileSpec) {
  return XPCOMUtils.generateModule([MicrosummaryService]);
}