Skip to main content
summaryrefslogtreecommitdiffstats
blob: 8bad0d84a73f2368d6157953e77e77f10b2fbfe3 (plain) (blame)
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
/*******************************************************************************
 * Copyright (c) 2005 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials 
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
/*
 *  $RCSfile: BeanInfoCacheController.java,v $
 *  $Revision: 1.19 $  $Date: 2006/05/23 15:43:06 $ 
 */
package org.eclipse.jem.internal.beaninfo.core;

import java.io.*;
import java.util.*;
import java.util.logging.Level;

import org.eclipse.core.resources.*;
import org.eclipse.core.runtime.*;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.emf.common.notify.*;
import org.eclipse.emf.common.notify.impl.AdapterImpl;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.change.ChangeDescription;
import org.eclipse.emf.ecore.change.impl.EObjectToChangesMapEntryImpl;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.emf.ecore.xmi.XMLResource;
import org.eclipse.jdt.core.*;

import org.eclipse.jem.internal.beaninfo.adapters.*;
import org.eclipse.jem.internal.java.beaninfo.IIntrospectionAdapter;
import org.eclipse.jem.java.JavaClass;
import org.eclipse.jem.util.emf.workbench.ProjectResourceSet;
import org.eclipse.jem.util.logger.proxy.Logger;
import org.eclipse.jem.util.plugin.JEMUtilPlugin;
import org.eclipse.jem.util.plugin.JEMUtilPlugin.CleanResourceChangeListener;

/**
 * Controller of the BeanInfo cache. There is one per workspace (it is a static).
 * 
 * The cache is stored on a per IPackageFragmentRoot basis. Each package fragment root will be:
 * 
 * <pre>
 * 
 *  root{999}/classname.xmi
 *  
 * </pre>
 * 
 * "root{999}" will be a unigue name (root appended with a number}, one for each package fragment root. The "classname.xmi" will be the BeanInfo cache
 * for a class in the root. A root can't have more than one class with the same name, so there shouldn't be any collisions.
 * <p>
 * Now roots can either be in a project, or they can be an external jar (which can be shared between projects).
 * <p>
 * Now all roots for a project will be stored in the project's working location
 * {@link org.eclipse.core.resources.IProject#getWorkingLocation(java.lang.String)}under the ".cache" directory. It will be this format in each
 * project location (under the org.eclipse.jem.beaninfo directory):
 * 
 * <pre>
 * 
 *  .index
 *  root{999}/...
 *  
 * </pre>
 * 
 * The ".index" file will be stored/loaded through an ObjectStream. It will be a {@link BeanInfoCacheController.Index}. It is the index to all of the
 * root's in the directory.
 * <p>
 * All of the external jar roots will be stored in the org.eclipse.jem.beaninfo plugin's state location
 * {@link org.eclipse.core.runtime.Platform#getStateLocation(org.osgi.framework.Bundle)}under the ".cache" directory. The format of this directory
 * will be the same as for each project. And the roots will be for each unique shared external jar (such as the differnt jre's rt.jars).
 * <p>
 * Note: There are so many places where synchronization is needed, so it is decided to synchronize only on BeanInfoCacheController.INSTANCE. It would
 * be too easy to get a dead-lock because the order of synchronizations can't be easily controlled. Since each piece of sync control is very short
 * (except for save of the indices, but that is ok because we don't want them changing while saving) it shouldn't block a lot. There is one place we
 * violate this and that is we do a sync on ClassEntry instance when working with the pending. This is necessary because we don't want the cache write
 * job to hold up everything while writing, so we sync on the entry being written instead. There we must be very careful that we never to
 * BeanInfoCacheControler.INSTANCE sync and then a ClassEntry sync because we could deadlock. The CE access under the covers may do a CE sync and then
 * a BeanInfoCacheController.INSTANCE sync.
 * 
 * @since 1.1.0
 */
public class BeanInfoCacheController {

	/**
	 * Singleton cache controller.
	 * 
	 * @since 1.1.0
	 */
	public static final BeanInfoCacheController INSTANCE = new BeanInfoCacheController();

	private BeanInfoCacheController() {
		// Start up save participent. This only is used for saving indexes and shutdown. Currently the saved state delta
		// is of no interest. If a project is deleted while we were not up, then the project index would be gone, so
		// our data will automatically be gone for the project. 
		// If a class was deleted while the project's beaninfo was not active, the cache will still contain it. If the class ever came back it
		// would be stale and so recreated. If it never comes back, until a clean is done, it would just hang around.
		// The problem with delete is it is hard to determine that the file is actually a class of interest. The javamodel
		// handles that for us but we won't have a javamodel to handle this on start up to tell us the file was a class of interest. So
		// we'll take the hit of possible cache for non-existant classes. A clean will take care of this.
		saveParticipant = new SaveParticipant();
		try {
			ResourcesPlugin.getWorkspace().addSaveParticipant(BeaninfoPlugin.getPlugin(), saveParticipant);
		} catch (CoreException e) {
			BeaninfoPlugin.getPlugin().getLogger().log(e.getStatus());
		}
		
		// Create a cleanup listener to handle clean requests and project deletes. We need to know about project deletes while
		// active because we may have a project index in memory and that needs to be thrown away.
		JEMUtilPlugin.addCleanResourceChangeListener(new CleanResourceChangeListener() {
		
			protected void cleanProject(IProject project) {
				// Get rid of the project index and the data for the project.
				synchronized (BeanInfoCacheController.this) {
					try {
						Index projectIndex = (Index) project.getSessionProperty(PROJECT_INDEX_KEY);
						if (projectIndex != null) {
							project.setSessionProperty(PROJECT_INDEX_KEY, null);
							projectIndex.markDead();
							cleanDirectory(getCacheDir(project).toFile(), true);
						}
						BeaninfoNature nature = BeaninfoPlugin.getPlugin().getNature(project);
						if (nature != null) {
							nature.projectCleaned();
							BeaninfoAdapterFactory adapterFactory = (BeaninfoAdapterFactory) EcoreUtil.getAdapterFactory(nature.getResourceSet().getAdapterFactories(), IIntrospectionAdapter.ADAPTER_KEY);
							if (adapterFactory != null) {
								adapterFactory.markAllStale(true);	// Also clear the overrides.
							}
						}						
					} catch (CoreException e) {
						// Shouldn't occur. 
					}
				}
			}
			
			protected void cleanAll() {
				synchronized(BeanInfoCacheController.this) {
					// Get MAIN_INDEX, mark it dead, and then delete everything under it.
					if (MAIN_INDEX != null) {
						MAIN_INDEX.markDead();
						MAIN_INDEX = null;
						cleanDirectory(getCacheDir(null).toFile(), true);
					}
				}
				super.cleanAll();
			}
			
			public void resourceChanged(IResourceChangeEvent event) {
				// We don't need to handle PRE_CLOSE because SaveParticipent project save will handle closing.
				switch (event.getType()) {
					case IResourceChangeEvent.PRE_DELETE:
						// Don't need to clear the cache directory because Eclipse will get rid of it.
						synchronized (BeanInfoCacheController.this) {
							try {
								Index projectIndex = (Index) event.getResource().getSessionProperty(PROJECT_INDEX_KEY);
								if (projectIndex != null) {
									// No need to remove from the project because the project is going away and will clean itself up.
									projectIndex.markDead();
								}
							} catch (CoreException e) {
								// Shouldn't occur.
							}
						}
						// Flow into PRE_CLOSE to release the nature.
					case IResourceChangeEvent.PRE_CLOSE:
						// About to close or delete, so release the nature, if any.
						IProject project = (IProject) event.getResource();
						BeaninfoNature nature = BeaninfoPlugin.getPlugin().getNature(project);
						if (nature != null) {
							nature.cleanup(false, true);
						}
						break;
					default:
						super.resourceChanged(event);
						break;
				}
			}
		
		}, IResourceChangeEvent.PRE_DELETE | IResourceChangeEvent.PRE_CLOSE);
	}

	protected SaveParticipant saveParticipant;

	/**
	 * An index structure for the Main and Project indexes. Access to the index contents and methods should synchronize on the index itself.
	 * <p>
	 * Getting to the index instance should only be through the <code>getMainIndex()</code> and <code>getProjectIndex(IProject)</code> accessors
	 * so that synchronization and serialization is controlled.
	 * 
	 * @since 1.1.0
	 */
	protected static class Index implements Serializable {

		private static final long serialVersionUID = 1106864425567L;

		/*
		 * Is this a dirty index, i.e. it has been changed and needs to be saved.
		 */
		transient private boolean dirty;

		private static final int DEAD = -1;	// Used in highRootNumber to indicate the index is dead.
		/**
		 * The highest root number used. It is incremented everytime one is needed. It doesn't ever decrease to recover removed roots.
		 * 
		 * @since 1.1.0
		 */
		public int highRootNumber;

		/**
		 * Map of root names to the root Index. The key is a {@link IPath}. The path will be relative to the workspace if a project root, or an
		 * absolute local path to the archive if it is an external archive. It is the IPath to package fragment root (either a folder or a jar file).
		 * <p>
		 * The value will be a {@link BeanInfoCacheController.RootIndex}. This is the index for the contents of that root.
		 * 
		 * @since 1.1.0
		 */
		transient public Map rootToRootIndex;

		/**
		 * @param dirty
		 *            The dirty to set.
		 * 
		 * @since 1.1.0
		 */
		public void setDirty(boolean dirty) {
			synchronized (BeanInfoCacheController.INSTANCE) {
				this.dirty = dirty;
			}
		}

		/**
		 * @return Returns the dirty.
		 * 
		 * @since 1.1.0
		 */
		public boolean isDirty() {
			synchronized (BeanInfoCacheController.INSTANCE) {
				return dirty;
			}
		}
		
		/**
		 * Answer if this index is dead. It is dead if a clean has occurred. This is needed because there could be some ClassEntry's still
		 * around (such as in the pending write queue) that are for cleaned roots. This is used to test if it has been cleaned.
		 * @return
		 * 
		 * @since 1.1.0
		 */
		public boolean isDead() {
			return highRootNumber == DEAD;
		}
		
		/**
		 * Mark the index as dead.
		 * 
		 * 
		 * @since 1.1.0
		 */
		void markDead() {
			highRootNumber = DEAD;
		}

		private void writeObject(ObjectOutputStream os) throws IOException {
			os.defaultWriteObject();
			// Now write out the root to root index map. We are not serializing the Map directly using normal Map serialization because
			// the key of the map is an IPath (which is a Path under the covers) and Path is not serializable.
			os.writeInt(rootToRootIndex.size());
			for (Iterator mapItr = rootToRootIndex.entrySet().iterator(); mapItr.hasNext();) {
				Map.Entry entry = (Map.Entry) mapItr.next();
				os.writeUTF(((IPath) entry.getKey()).toString());
				os.writeObject(entry.getValue());
			}
		}

		private void readObject(ObjectInputStream is) throws IOException, ClassNotFoundException {
			is.defaultReadObject();
			int size = is.readInt();
			rootToRootIndex = new HashMap(size < 100 ? 100 : size);
			while (size-- > 0) {
				rootToRootIndex.put(new Path(is.readUTF()), is.readObject());
			}
		}
	}

	/**
	 * An index for a root. It has an entry for each class in this root. The class cache entry describes the cache, whether it is stale, what the
	 * current mod stamp is, etc.
	 * 
	 * @since 1.1.0
	 */
	public static abstract class RootIndex implements Serializable {

		private static final long serialVersionUID = 1106868674101L;

		transient private IPath cachePath; // Absolute local filesystem IPath to the root cache directory. Computed at runtime because it may change
										   // if workspace relocated.

		protected Map classNameToClassEntry; // Map of class names to class entries.

		private String rootName; // Name of the root directory in the cache (e.g. "root1").

		protected Index index; // Index containing this root index.

		protected RootIndex() {
		}

		public RootIndex(String rootName, Index index) {
			this.rootName = rootName;
			classNameToClassEntry = new HashMap(100); // When created brand new, put in a map. Otherwise object stream will create the map.
			this.index = index;
		}
		
		/**
		 * Get the index that points to this root.
		 * @return
		 * 
		 * @since 1.1.0
		 */
		Index getIndex() {
			return index;
		}

		/**
		 * Return the root directory name
		 * 
		 * @return rootname
		 * 
		 * @since 1.1.0
		 */
		public String getRootName() {
			return rootName;
		}

		/**
		 * Set this RootIndex (and the containing Index) as being dirty and in need of saving.
		 * 
		 * 
		 * @since 1.1.0
		 */
		public void setDirty() {
			index.setDirty(true);
		}

		/*
		 * Setup for index. It will initialize the path. Once set it won't set it again. This will be called repeatedly by the cache controller
		 * because there is no way to know if it was lazily created or was brought in from file. When brought in from file the path is not set because
		 * it should be relocatable and we don't want absolute paths out on the disk caches, and we don't want to waste time creating the path at load
		 * time because it may not be needed for a while. So it will be lazily created. <p> If the project is set, then the path will be relative to
		 * the project's working location. If project is <code> null </code> then it will be relative to the BeanInfo plugin's state location. <p>
		 * This is <package-protected> because only the creator (BeanInfoCacheController class) should set this up.
		 * 
		 * @param project
		 * 
		 * @since 1.1.0
		 */
		void setupIndex(IProject project) {
			if (getCachePath() == null)
				cachePath = getCacheDir(project).append(rootName);
		}

		/**
		 * @return Returns the path of the cache directory for the root.
		 * 
		 * @since 1.1.0
		 */
		public IPath getCachePath() {
			return cachePath;
		}

		/**
		 * Return whether this is a root for a archive or a folder.
		 * 
		 * @return <code>true</code> if archive for a root. <code>false</code> if archive for a folder.
		 * 
		 * @since 1.1.0
		 */
		public abstract boolean isArchiveRoot();
	}

	/**
	 * A root index that is for an archive, either internal or external. It contains the archive's modification stamp. Each class cache entry will
	 * have this same modification stamp. If the archive is changed then all of the class cache entries will be removed because they are all possibly
	 * stale. No way to know which may be stale and which not.
	 * 
	 * @since 1.1.0
	 */
	public static class ArchiveRootIndex extends RootIndex {

		private static final long serialVersionUID = 110686867444L;

		private long archiveModificationStamp;

		/*
		 * For serializer to call.
		 */
		protected ArchiveRootIndex() {
		}

		public ArchiveRootIndex(String rootName, long archiveModificationStamp, Index index) {
			super(rootName, index);
			this.archiveModificationStamp = archiveModificationStamp;
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see org.eclipse.jem.internal.beaninfo.core.BeanInfoCacheController.RootIndex#isArchiveRoot()
		 */
		public boolean isArchiveRoot() {
			return true;
		}

		/*
		 * Set the modification stamp. <p> <package-protected> because only the cache controller should change it.
		 * 
		 * @param archiveModificationStamp The archiveModificationStamp to set.
		 * 
		 * @see BeanInfoCacheController#MODIFICATION_STAMP_STALE
		 * @since 1.1.0
		 */
		void setArchiveModificationStamp(long archiveModificationStamp) {
			this.archiveModificationStamp = archiveModificationStamp;
			setDirty();
		}

		/**
		 * Returns the modification stamp.
		 * 
		 * @return Returns the archiveModificationStamp.
		 * @see BeanInfoCacheController#MODIFICATION_STAMP_STALE
		 * @since 1.1.0
		 */
		public long getArchiveModificationStamp() {
			return archiveModificationStamp;
		}
	}

	/**
	 * This is a root index for a folder (which will be in the workspace). Each class cache entry can have a different modification stamp with a
	 * folder root index.
	 * 
	 * @since 1.1.0
	 */
	public static class FolderRootIndex extends RootIndex {

		private static final long serialVersionUID = 1106868674922L;

		/*
		 * For serialization.
		 */
		protected FolderRootIndex() {
		}

		/**
		 * @param rootName
		 * 
		 * @since 1.1.0
		 */
		public FolderRootIndex(String rootName, Index index) {
			super(rootName, index);
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see org.eclipse.jem.internal.beaninfo.core.BeanInfoCacheController.RootIndex#isArchiveRoot()
		 */
		public boolean isArchiveRoot() {
			return false;
		}
	}

	/**
	 * An individual class entry from the cache. It has an entry for each class in the root. The class cache entry describes the cache, whether it is
	 * stale, what the current mod stamp is, etc.
	 * <p>
	 * There is a method to call to see if deleted. This should only be called if entry is being held on to because
	 * <code>getClassEntry(JavaClass)</code> will never return a deleted entry. There is a method to get the modification stamp of the current cache
	 * entry. This is the time stamp of the cooresponding class resource (or archive file) that the cache file was created from. If the value is
	 * <code>IResource.NULL_STAMP</code>, then the cache file is known to be stale. Otherwise if the value is less than a modification stamp of a
	 * super class then the cache file is stale.
	 * 
	 * @see ClassEntry#isDeleted()
	 * @see ClassEntry#getModificationStamp()
	 * @see BeanInfoCacheController#getClassEntry(JavaClass)
	 * @since 1.1.0
	 */
	public static class ClassEntry implements Serializable {

		private static final long serialVersionUID = 1106868674333L;

		public static final long DELETED_MODIFICATION_STAMP = Long.MIN_VALUE; // This flag won't be seen externally. It is used to indicate the entry
																			  // has been deleted for those that have been holding a CE.
		
		/**
		 * Check against the super modification stamp and the interface stamps to see if they were set
		 * by undefined super class or interface at cache creation time. 
		 * 
		 * @since 1.1.0
		 */
		public static final long SUPER_UNDEFINED_MODIFICATION_STAMP = Long.MIN_VALUE+1;	

		private long modificationStamp;
		private long superModificationStamp;	// Stamp of superclass, if any, at time of cache creation.
		private String[] interfaceNames;	// Interfaces names (null if no interfaces)
		private long[] interfaceModicationStamps;	// Modification stamps of interfaces, if any. (null if no interfaces).
		private transient Resource pendingResource;	// Resource is waiting to be saved, but the timestamps are for this pending resource so that we know what it will be ahead of time. At this point the class will be introspected. 

		private RootIndex rootIndex; // The root index this class entry is in, so that any changes can mark the entry as dirty.

		private String className; // The classname for this entry.
		
		private boolean saveOperations;	// Flag for saving operations. Once this is set, it will continue to save operations in the cache in addition to everything else.

		private long configurationModificationStamp;	// Modification stamp of the Eclipse configuration. Used to determine if the cache of override files is out of date due to a config change.
		private boolean overrideCacheExists;	// Flag that there is an override cache to load. This is orthogonal to the config mod stamp because it simply means that on the current configuration there is no override cache.
		private transient Resource pendingOverrideResource;	// Override resource is waiting to be saved.
		
		protected ClassEntry() {
		}

		ClassEntry(RootIndex rootIndex, String className) {
			this.setRootIndex(rootIndex);
			this.className = className;
			modificationStamp = IResource.NULL_STAMP;
			rootIndex.classNameToClassEntry.put(className, this);
		}

		/**
		 * 
		 * @return
		 * 
		 * @since 1.1.0
		 */
		public String getClassName() {
			return className;
		}

		/**
		 * Return whether the entry has been deleted. This will never be seen in an entry that is still in an index. It is used for entries that have
		 * been removed from the index. It is for classes (such as the BeanInfoClassAdapter) which are holding onto a copy of entry to let them know.
		 * <p>
		 * Holders of entries should call isDeleted if they don't need to further check the mod stamp. Else they should call getModificationStamp and
		 * check of deleted, stale, or mod stamp. That would save extra synchronizations. If entry is deleted then it should throw the entry away.
		 * 
		 * @return <code>true</code> if the entry has been deleted, <code>false</code> if still valid.
		 * 
		 * @see ClassEntry#getModificationStamp()
		 * @see ClassEntry#isStale()
		 * @see ClassEntry#DELETED_MODIFICATION_STAMP
		 * @since 1.1.0
		 */
		public boolean isDeleted() {
			return getModificationStamp() == DELETED_MODIFICATION_STAMP;
		}

		/**
		 * Mark the entry as deleted. It will also be removed from root index in that case.
		 * <p>
		 * Note: It is public only so that BeanInfoClassAdapter can access it. It should not be called by anyone else outside of BeanInfo.
		 */
		public synchronized void markDeleted() {
			if (!isDeleted()) {
				getRootIndex().classNameToClassEntry.remove(className);
				setModificationStamp(DELETED_MODIFICATION_STAMP); // Also marks index as dirty.
			}
		}

		/**
		 * Return whether the entry is stale or not. This orthoganal to isDeleted. isDeleted will not be true if isStale and isStale will not be true
		 * if isDeleted. Normally you should use getModificationStamp and check the value for IResource.NULL_STAMP and other values to bring it down
		 * to only one synchronized call to CE, but if only needing to know if stale, you can use this.
		 * 
		 * @return
		 * 
		 * @since 1.1.0
		 * @see IResource#NULL_STAMP
		 * @see ClassEntry#getModificationStamp()
		 * @see ClassEntry#isDeleted()
		 */
		public boolean isStale() {
			return getModificationStamp() == IResource.NULL_STAMP;
		}

		/**
		 * Return the modification stamp. For those holding onto an entry, and they need to know more than if just deleted, then they should just the
		 * return value from getModificationStamp. Else they should use isDeleted or isStale.
		 * 
		 * @return modification stamp, or {@link IResource#NULL_STAMP}if stale or not yet created, or {@link ClassEntry#DELETED_MODIFICATION_STAMP}
		 *         if deleted.
		 * 
		 * @see ClassEntry#isDeleted()
		 * @see ClassEntry#isStale()
		 * @since 1.1.0
		 */
		public synchronized long getModificationStamp() {
			return modificationStamp;
		}
		
		/**
		 * Return the super modification stamp.
		 * @return
		 * 
		 * @since 1.1.0
		 */
		public synchronized long getSuperModificationStamp() {
			return superModificationStamp;
		}
		
		/**
		 * Return the interface names or <code>null</code> if no interface names.
		 * @return
		 * 
		 * @since 1.1.0
		 */
		public synchronized String[] getInterfaceNames() {
			return interfaceNames;
		}

		/**
		 * Return the interface modification stamps or <code>null</code> if no interfaces.
		 * @return
		 * 
		 * @since 1.1.0
		 */
		public synchronized long[] getInterfaceModificationStamps() {
			return interfaceModicationStamps;
		}
		
		/*
		 * Set the modification stamp. <p> <package-protected> because only the cache controller should set it. @param modificationStamp
		 * 
		 * @since 1.1.0
		 */
		void setModificationStamp(long modificationStamp) {
			if (this.modificationStamp != modificationStamp) {
				this.modificationStamp = modificationStamp;
				getRootIndex().setDirty();
			}
		}

		/**
		 * Answer whether operations are also stored in the cache for this class. By default they are not. Once turned on they
		 * will always be stored for this class until the class is deleted.
		 * 
		 * @return <code>true</code> if operations are cached, <code>false</code> if they are not cached.
		 * 
		 * @since 1.1.0
		 */
		public synchronized boolean isOperationsStored() {
			return saveOperations;
		}
		
		/*
		 * Set the operations stored flag.
		 * @param storeOperations
		 * 
		 * @see BeanInfoCacheController.ClassEntry#isOperationsStored()
		 * @since 1.1.0
		 */
		void setOperationsStored(boolean storeOperations) {
			saveOperations = storeOperations;
		}
		
		/**
		 * Get the configuration modification stamp of the last saved override cache.
		 * 
		 * @return
		 * 
		 * @since 1.1.0
		 */
		public synchronized long getConfigurationModificationStamp() {
			return configurationModificationStamp;
		}

		/* 
		 * Set the configuration modification stamp.
		 * <p> <package-protected> because only the cache controller should access it.
		 * 
		 * @param configurationModificationStamp
		 * 
		 * @since 1.1.0
		 */
		void setConfigurationModificationStamp(long configurationModificationStamp) {
			this.configurationModificationStamp = configurationModificationStamp;
			getRootIndex().setDirty();
		}
		
		/**
		 * Answer whether there is an override cache available.
		 * @return
		 * 
		 * @since 1.1.0
		 */
		public synchronized boolean overrideCacheExists() {
			return overrideCacheExists;
		}
		
		/*
		 * Set the override cache exists flag.
		 * <p> <package-protected> because only the cache controller should access it.
		 * @param overrideCacheExists
		 * 
		 * @since 1.1.0
		 */
		void setOverrideCacheExists(boolean overrideCacheExists) {
			this.overrideCacheExists = overrideCacheExists;
		}
		
		/**
		 * Get the pending resource or <code>null</code> if not pending.
		 * 
		 * @return the pending resource or <code>null</code> if not pending.
		 * 
		 * @since 1.1.0
		 */
		public synchronized Resource getPendingResource() {
			return pendingResource;
		}
		
		/*
		 * Set the entry. The sequence get,do something,set must be grouped within a synchronized(ClassEntry). 
		 * <p> <package-protected> because only the cache controller should access it. 
		 * @param cacheResource The cacheResource to set.
		 * 
		 * @since 1.1.0
		 */
		void setPendingResource(Resource pendingResource) {
			this.pendingResource = pendingResource;
		}

		/**
		 * Get the pending override resource or <code>null</code> if not pending.
		 * 
		 * @return the pending override resource or <code>null</code> if not pending.
		 * 
		 * @since 1.1.0
		 */
		public synchronized Resource getPendingOverrideResource() {
			return pendingOverrideResource;
		}
		
		/*
		 * Set the entry. The sequence get,do something,set must be grouped within a synchronized(ClassEntry). 
		 * <p> <package-protected> because only the cache controller should access it. 
		 * @param cacheResource The cacheResource to set.
		 * 
		 * @since 1.1.0
		 */
		void setPendingOverrideResource(Resource pendingOverrideResource) {
			this.pendingOverrideResource = pendingOverrideResource;
		}

		/*
		 * <package-protected> because only the cache controller should access it. @param rootIndex The rootIndex to set.
		 * 
		 * @since 1.1.0
		 */
		void setRootIndex(RootIndex rootIndex) {
			this.rootIndex = rootIndex;
		}

		/*
		 * <package-protected> because only the cache controller should access it. @return Returns the rootIndex.
		 * 
		 * @since 1.1.0
		 */
		RootIndex getRootIndex() {
			return rootIndex;
		}
		
		/*
		 * <package-protected> because only the cache controller should access it.
		 * 
		 * @since 1.1.0
		 */
		void setSuperModificationStamp(long superModificationStamp) {
			this.superModificationStamp = superModificationStamp;
		}

		/*
		 * <package-protected> because only the cache controller should access it.
		 * 
		 * @since 1.1.0
		 */
		void setInterfaceNames(String[] interfaceNames) {
			this.interfaceNames = interfaceNames;
		}

		/*
		 * <package-protected> because only the cache controller should access it.
		 * 
		 * @since 1.1.0
		 */
		void setInterfaceModificationStamps(long[] interfaceModificationStamps) {
			this.interfaceModicationStamps = interfaceModificationStamps;
		}
		
	}

	/*
	 * Main index for the external jars. This variable should not be referenced directly except through the getMainIndex() accessor. That controls
	 * synchronization and restoration as needed.
	 */
	private static Index MAIN_INDEX;

	/*
	 * Key into the Project's session data for the project index. The Project index is stored in the project's session data. That
	 * way when the project is closed or deleted it will go away. 
	 * 
	 * The project indexes will be read in as needed on a per-project basis. This variable should not be
	 * referenced directly except through the getProjectIndex(IProject) accessor. That controls synchronization and restoration as needed.
	 * Only during cleanup and such where we don't want to create one if it doesn't exist you must use sync(this). Be careful to keep 
	 * the sync small.
	 */
	private static final QualifiedName PROJECT_INDEX_KEY = new QualifiedName(BeaninfoPlugin.PI_BEANINFO_PLUGINID, "project_index");	//$NON-NLS-1$

	/*
	 * Suffix for class cache files.
	 */
	private static final String CLASS_CACHE_SUFFIX = ".xmi"; //$NON-NLS-1$
	/*
	 * Suffic for class override cache files.
	 */
	private static final String OVERRIDE_CACHE_SUFFIX = ".override.xmi"; //$NON-NLS-1$

	/**
	 * Return the current class entry for the JavaClass, or <code>null</code> if no current entry.
	 * 
	 * @param jclass
	 * @return class entry or <code>null</code> if no current entry.
	 * 
	 * @since 1.1.0
	 */
	public ClassEntry getClassEntry(JavaClass jclass) {
		IType type = (IType) jclass.getReflectionType();
		RootIndex rootIndex = getRootIndex(type);
		String className = jclass.getQualifiedNameForReflection();
		return getClassEntry(rootIndex, className, false);
	}

	/**
	 * Enumeration for newCache: Signals that this cache is the Reflection Cache with no operations in it.
	 * @since 1.1.0
	 */
	public final static int REFLECTION_CACHE = 1;
	/**
	 * Enumeration for newCache: Signals that this cache is the Reflection Cache with operations in it.
	 * @since 1.1.0
	 */
	public final static int REFLECTION_OPERATIONS_CACHE = 2;
	/**
	 * Enumeration for newCache: Signals that this cache is the Overrides cache.
	 * @since 1.1.0
	 */
	public final static int OVERRIDES_CACHE = 3;
	/**
	 * A new cache entry for the given class has been created. Need to write it out.
	 * 
	 * @param jclass
	 *            the JavaClass the cache is for.
	 * @param cache
	 *            the ChangeDescription to put out if cacheType is Reflection types, a List if override cache type.
	 * @param cacheType
	 *            {@link BeanInfoCacheController.ClassEntry#REFLECTION_CACHE} for the enum values.
	 * @return new class entry (or old one if same one). Should always replace one being held by this one. <code>null</code> if cache could not be
	 *         updated for some reason.
	 * @since 1.1.0
	 */
	public ClassEntry newCache(JavaClass jclass, Object cache, int cacheType) {
		if (BeaninfoPlugin.getPlugin().getLogger().isLoggingLevel(Level.FINER)) {
			Logger logger = BeaninfoPlugin.getPlugin().getLogger();
			String type = cacheType!=OVERRIDES_CACHE?"Class":"Overrides"; //$NON-NLS-1$ //$NON-NLS-2$
			if (cacheType == OVERRIDES_CACHE && cache == null)
				type+="  empty"; //$NON-NLS-1$
			logger.log("Creating cache for class "+jclass.getQualifiedNameForReflection()+" cache type="+type, Level.FINER); //$NON-NLS-1$ //$NON-NLS-2$
		}
		ChangeDescription cd = null;
		if (cacheType != OVERRIDES_CACHE) {
			// First go through the cd and remove any empty changes. This is because we created the feature change before we knew what went into
			// it, and at times nothing goes into it.
			cd = (ChangeDescription) cache;
			for (Iterator iter = cd.getObjectChanges().iterator(); iter.hasNext();) {
				EObjectToChangesMapEntryImpl fcEntry = (EObjectToChangesMapEntryImpl) iter.next();
				if (((List) fcEntry.getValue()).isEmpty())
					iter.remove(); // Empty changes, remove it.
			}
		}
		IType type = (IType) jclass.getReflectionType();
		RootIndex rootIndex = getRootIndex(type);
		String className = jclass.getQualifiedNameForReflection();
		ClassEntry ce = getClassEntry(rootIndex, className, true); // Create it if not existing.
		// Sync on ce so that only can create a cache for a class at a time and so that writing (if occurring at the same time for the class) can be
		// held up.
		// this is a violation of the agreement to only sync on THIS, but it is necessary or else the write job would lock everything out while it is
		// writing. This way it only locks out one class, if the class is at the same time.
		// We shouldn't have deadlock because here we lock ce and then THIS (maybe). The write job will lock ce, and then lock THIS. Everywhere else
		// we must
		// also do lock ce then THIS. Mustn't do other way around or possibility of deadlock. Be careful that any synchronized methods in this class
		// do
		// not lock an existing ce.
		ResourceSet cacheRset = null;
		synchronized (ce) {
			Resource cres;
			if (cacheType != OVERRIDES_CACHE) {
				cres = ce.getPendingResource();
				if (cres != null) {
					// We have a pending, so clear and reuse the resource.
					cres.getContents().clear();
				} else {
					// Not currently writing or waiting to write, so create a new resource.
					cres = jclass.eResource().getResourceSet().createResource(
							URI.createFileURI(rootIndex.getCachePath().append(className + CLASS_CACHE_SUFFIX).toString()));
				}
				cacheRset = cres.getResourceSet();
				ce.setOperationsStored(cacheType == REFLECTION_OPERATIONS_CACHE);
				ce.setPendingResource(cres);
				cres.getContents().add(cd);
				// Archives use same mod as archive (retrieve from rootindex), while non-archive use the underlying resource's mod stamp.
				if (rootIndex.isArchiveRoot())
					ce.setModificationStamp(((ArchiveRootIndex) rootIndex).getArchiveModificationStamp());
				else {
					try {
						ce.setModificationStamp(type.getUnderlyingResource().getModificationStamp());
					} catch (JavaModelException e) {
						BeaninfoPlugin.getPlugin().getLogger().log(e);
						ce.markDeleted(); // Mark as deleted in case this was an existing that someone is holding.
						return null; // Couldn't do it, throw cache entry away. This actually should never occur.
					}
				}
				// Need to get the supers info. 
				List supers = jclass.getESuperTypes();
				if (!supers.isEmpty()) {
					// We assume that they all have been introspected. This was done back in main introspection. If they are introspected they will have a class entry.
					BeaninfoClassAdapter bca = BeaninfoClassAdapter.getBeaninfoClassAdapter((EObject) supers.get(0));
					ClassEntry superCE = bca.getClassEntry();
					if (superCE != null)
						ce.setSuperModificationStamp(superCE.getModificationStamp());
					else
						ce.setSuperModificationStamp(ClassEntry.SUPER_UNDEFINED_MODIFICATION_STAMP);	// No classentry means undefined. So put something in so that when it becomes defined we will know.
					if(supers.size() == 1) {
						ce.setInterfaceNames(null);
						ce.setInterfaceModificationStamps(null);
					} else {
						String[] interNames = new String[supers.size()-1];
						long[] interMods = new long[interNames.length];
						for (int i = 1, indx = 0; indx < interNames.length; i++, indx++) {
							JavaClass javaClass = (JavaClass) supers.get(i);
							bca = BeaninfoClassAdapter.getBeaninfoClassAdapter(javaClass);
							bca.introspectIfNecessary();	// Force introspection to get a valid super mod stamp.
							superCE = bca.getClassEntry();
							interNames[indx] = javaClass.getQualifiedNameForReflection();
							if (superCE != null)
								interMods[indx] = superCE.getModificationStamp();
							else
								interMods[indx] = ClassEntry.SUPER_UNDEFINED_MODIFICATION_STAMP;	// No classentry means undefined. So put something in so that when it becomes defined we will know.
						}
						ce.setInterfaceNames(interNames);
						ce.setInterfaceModificationStamps(interMods);
					}
				} else {
					ce.setSuperModificationStamp(IResource.NULL_STAMP);
					ce.setInterfaceNames(null);
					ce.setInterfaceModificationStamps(null);
				}
			} else {
				// We are an override cache.
				if (cache != null) { 
					cres = ce.getPendingOverrideResource();
					if (cres != null) {
						// We have a pending, so clear and reuse the resource.
						cres.getContents().clear();
					} else {
						// Not currently writing or waiting to write, so create a new resource.
						cres = jclass.eResource().getResourceSet().createResource(
								URI.createFileURI(rootIndex.getCachePath().append(className + OVERRIDE_CACHE_SUFFIX).toString()));
					}
					cacheRset = cres.getResourceSet();
					cres.getContents().addAll((List) cache);
					ce.setPendingOverrideResource(cres);
					ce.setOverrideCacheExists(true);
				} else {
					ce.setPendingOverrideResource(null);
					ce.setOverrideCacheExists(false);
				}
				ce.setConfigurationModificationStamp(Platform.getPlatformAdmin().getState(false).getTimeStamp());
			}
		}
		queueClassEntry(ce, cacheRset); // Now queue it up.
		return ce;
	}

	/**
	 * Get the cache resource for the given java class.
	 * <p>
	 * NOTE: It is the responsibility of the caller to ALWAYS remove the Resource from its resource set when done with it.
	 * 
	 * @param jclass
	 * @param ce the class entry for the jclass
	 * @param reflectCache <code>true</code> if this the reflection/introspection cache or <code>false</code> if this is the override cache.
	 * @return the loaded cache resource, or <code>null</code> if not there (for some reason) or an error trying to load it.
	 * 
	 * @since 1.1.0
	 */
	public Resource getCache(JavaClass jclass, ClassEntry ce, boolean reflectCache) {
		String className = jclass.getQualifiedNameForReflection();
		if (BeaninfoPlugin.getPlugin().getLogger().isLoggingLevel(Level.FINER)) {
			Logger logger = BeaninfoPlugin.getPlugin().getLogger();
			String type = reflectCache?"Class":"Overrides"; //$NON-NLS-1$ //$NON-NLS-2$
			logger.log("Loading cache for class "+className+" cache type="+type, Level.FINER); //$NON-NLS-1$ //$NON-NLS-2$
		}

		if (reflectCache) {
			boolean waitForJob = false;
			synchronized (ce) {
				if (ce.getPendingResource() != null) {
					// We have one pending. So wait until write cache job is done, and then load it in.
					// Note: Can't just copy the pending resource because it has references to JavaClasses
					// and these could be in a different project (since this could be a workspace wide class).
					// We would get the wrong java classes then when we apply it. 
					waitForJob = true;
					if (BeaninfoPlugin.getPlugin().getLogger().isLoggingLevel(Level.FINER))
						BeaninfoPlugin.getPlugin().getLogger().log("Using pending class cache.", Level.FINER); //$NON-NLS-1$
				}
			}
			if (waitForJob)
				waitForCacheSaveJob();	

			try {
				return jclass.eResource().getResourceSet().getResource(
						URI.createFileURI(ce.getRootIndex().getCachePath().append(
								className + CLASS_CACHE_SUFFIX).toString()), true);
			} catch (Exception e) {
				// Something happened and couldn't load it.
				// TODO - need to remove the Level.INFO arg when the beaninfo cache is working dynamically
				BeaninfoPlugin.getPlugin().getLogger().log(e, Level.INFO);
				return null;
			}
		} else {
			boolean waitForJob = false;
			synchronized (ce) {
				if (ce.getPendingOverrideResource() != null) {
					// We have one pending. So wait until write cache job is done, and then load it in.
					// Note: Can't just copy the pending resource because it has references to JavaClasses
					// and these could be in a different project (since this could be a workspace wide class).
					// We would get the wrong java classes then when we apply it. 
					waitForJob = true;
					if (BeaninfoPlugin.getPlugin().getLogger().isLoggingLevel(Level.FINER))
						BeaninfoPlugin.getPlugin().getLogger().log("Using pending override cache.", Level.FINER); //$NON-NLS-1$
				}
			}
			if (waitForJob)
				waitForCacheSaveJob();	

			try {
				return jclass.eResource().getResourceSet().getResource(
						URI.createFileURI(ce.getRootIndex().getCachePath().append(
								className + OVERRIDE_CACHE_SUFFIX).toString()), true);
			} catch (Exception e) {
				// Something happened and couldn't load it.
				// TODO - need to remove the Level.INFO arg when the beaninfo cache is working dynamically
				BeaninfoPlugin.getPlugin().getLogger().log(e, Level.INFO);
				return null;
			}
		}
	}

	private synchronized ClassEntry getClassEntry(RootIndex rootIndex, String className, boolean createEntry) {
		ClassEntry ce = (ClassEntry) rootIndex.classNameToClassEntry.get(className);
		if (createEntry && ce == null) {
			// Need to create one.
			ce = new ClassEntry(rootIndex, className);
			// Don't actually mark the rootIndex dirty until the cache for it is actually saved out.
		}
		return ce;
	}

	private static final String ROOT_PREFIX = "root"; //$NON-NLS-1$

	/*
	 * Get the root index for the appropriate cache for the given java class.
	 */
	private RootIndex getRootIndex(IType type) {
		IPackageFragmentRoot root = (IPackageFragmentRoot) type.getAncestor(IJavaElement.PACKAGE_FRAGMENT_ROOT);
		if (!root.isExternal()) {
			// So it is in a project. Get the project index.
			return getRootIndex(root, root.getJavaProject().getProject());
		} else {
			// It is an external jar (archive), so needs to come from main index, no project.
			return getRootIndex(root, null);
		}
	}

	/*
	 * Get the root index for the given root. A Project index if project is not null.
	 */
	private synchronized RootIndex getRootIndex(IPackageFragmentRoot root, IProject project) {
		Index index = project != null ? getProjectIndex(project) : getMainIndex();
		IPath rootPath = root.getPath();
		RootIndex rootIndex = (RootIndex) index.rootToRootIndex.get(rootPath);
		if (rootIndex == null) {
			// Need to do a new root path.
			String rootName = ROOT_PREFIX + (++index.highRootNumber);
			rootIndex = root.isArchive() ? createArchiveRootIndex(root, rootName, index) : new FolderRootIndex(rootName, index);
			index.rootToRootIndex.put(rootPath, rootIndex);
			// Don't set index dirty until we actually save a class cache file. Until then it only needs to be in memory.
		}
		rootIndex.setupIndex(project); // Set it up, or may already be set, so it will do nothing in that case.
		return rootIndex;
	}

	/*
	 * Create an archive root with the given root number and root.
	 */
	private RootIndex createArchiveRootIndex(IPackageFragmentRoot rootArchive, String rootName, Index index) {
		long modStamp = IResource.NULL_STAMP;
		if (rootArchive.isExternal()) {
			modStamp = rootArchive.getPath().toFile().lastModified();
		} else {
			try {
				modStamp = rootArchive.getUnderlyingResource().getModificationStamp();
			} catch (JavaModelException e) {
				BeaninfoPlugin.getPlugin().getLogger().log(e);
			}
		}
		return new ArchiveRootIndex(rootName, modStamp, index);
	}

	private static final String INDEXFILENAME = ".index"; //$NON-NLS-1$

	private static final String CACHEDIR = ".cache"; // Cache directory (so as not to conflict with any future BeanInfo Plugin specific data files. //$NON-NLS-1$

	/*
	 * Get the cache directory for the project (or if project is null, the main plugin cache directory).
	 */
	// TODO: make this one private
	public static IPath getCacheDir(IProject project) {
		if (project != null)
			return project.getWorkingLocation(BeaninfoPlugin.getPlugin().getBundle().getSymbolicName()).append(CACHEDIR);
		else
			return BeaninfoPlugin.getPlugin().getStateLocation().append(CACHEDIR);
	}

	/*
	 * Get the project index. Synchronized so that we can create it if necessary and not get race conditions.
	 */
	private synchronized Index getProjectIndex(IProject project) {
		try {
			Index index = (Index) project.getSessionProperty(PROJECT_INDEX_KEY);
			if (index == null) {
				// Read the index in.
				File indexDirFile = getCacheDir(project).append(INDEXFILENAME).toFile();
				if (indexDirFile.canRead()) {
					ObjectInputStream ois = null;
					try {
						ois = new ObjectInputStream(new BufferedInputStream(new FileInputStream(indexDirFile)));
						index = (Index) ois.readObject();
					} catch (InvalidClassException e) {
						// This is ok. It simply means the cache index is at a downlevel format and needs to be reconstructed.
					} catch (IOException e) {
						BeaninfoPlugin.getPlugin().getLogger().log(e);
					} catch (ClassNotFoundException e) {
						BeaninfoPlugin.getPlugin().getLogger().log(e);
					} finally {
						if (ois != null)
							try {
								ois.close();
							} catch (IOException e) {
								BeaninfoPlugin.getPlugin().getLogger().log(e);
							}
					}
				}

				if (index == null) {
					// Doesn't yet exist or it couldn't be read for some reason, or it was downlevel cache in which case we just throw it away and create
					// new).
					index = new Index();
					index.highRootNumber = 0;
					index.rootToRootIndex = new HashMap();
				}

				project.setSessionProperty(PROJECT_INDEX_KEY, index); // We either created a new one, or we were able to load it.
			}
			return index;
		} catch (CoreException e) {
			// Shouldn't occur,
			return null;
		}
	}

	/*
	 * Get the main index. Synchronized so that we can create it if necessary and not get race conditions.
	 */
	private synchronized Index getMainIndex() {
		if (MAIN_INDEX == null) {
			// Read the index in.
			File indexDirFile = getCacheDir(null).append(INDEXFILENAME).toFile();
			if (indexDirFile.canRead()) {
				ObjectInputStream ois = null;
				try {
					ois = new ObjectInputStream(new BufferedInputStream(new FileInputStream(indexDirFile)));
					MAIN_INDEX = (Index) ois.readObject();
				} catch (InvalidClassException e) {
					// This is ok. It just means that the cache index is at a downlevel format and needs to be reconstructed.
				} catch (IOException e) {
					BeaninfoPlugin.getPlugin().getLogger().log(e);
				} catch (ClassNotFoundException e) {
					BeaninfoPlugin.getPlugin().getLogger().log(e);
				} finally {
					if (ois != null)
						try {
							ois.close();
						} catch (IOException e) {
							BeaninfoPlugin.getPlugin().getLogger().log(e);
						}
				}
			}

			if (MAIN_INDEX == null) {
				// Doesn't yet exist or it couldn't be read for some reason, or it was downlevel cache in which case we just throw it away and create
				// new).
				MAIN_INDEX = new Index();
				MAIN_INDEX.highRootNumber = 0;
				MAIN_INDEX.rootToRootIndex = new HashMap();
			}

		}
		return MAIN_INDEX;
	}

	// -------------- Save Participant code -----------------

	protected class SaveParticipant implements ISaveParticipant {

		/*
		 * (non-Javadoc)
		 * 
		 * @see org.eclipse.core.resources.ISaveParticipant#doneSaving(org.eclipse.core.resources.ISaveContext)
		 */
		public void doneSaving(ISaveContext context) {
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see org.eclipse.core.resources.ISaveParticipant#prepareToSave(org.eclipse.core.resources.ISaveContext)
		 */
		public void prepareToSave(ISaveContext context) throws CoreException {
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see org.eclipse.core.resources.ISaveParticipant#rollback(org.eclipse.core.resources.ISaveContext)
		 */
		public void rollback(ISaveContext context) {
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see org.eclipse.core.resources.ISaveParticipant#saving(org.eclipse.core.resources.ISaveContext)
		 */
		public void saving(ISaveContext context) throws CoreException {
			boolean fullsave = false;
			switch (context.getKind()) {
				case ISaveContext.PROJECT_SAVE:
					IProject project = context.getProject();
					synchronized (BeanInfoCacheController.INSTANCE) {
						// Write the index. The cache save job will eventually run and at that point write out the pending cache files too.
						// They don't need to be written before the project save is complete.
						Index projectIndex = (Index) project.getSessionProperty(PROJECT_INDEX_KEY);
						if (projectIndex != null && projectIndex.isDirty())
							if (reconcileIndexDirectory(project, projectIndex))
								writeIndex(project, projectIndex);
							else {
								// It was empty, just get rid of the index. The directories have already been cleared.
								projectIndex.markDead();
								project.setSessionProperty(PROJECT_INDEX_KEY, null);
							}
					}
					break;
				case ISaveContext.FULL_SAVE:
					fullsave = true;
					waitForCacheSaveJob();
				// Now flow into the snapshot save to complete the fullsave.
				case ISaveContext.SNAPSHOT:
					// For a snapshot, just the dirty indexes, no clean up. If fullsave, cleanup the indexes, but only save the dirty.
					synchronized (BeanInfoCacheController.INSTANCE) {
						if (MAIN_INDEX != null) {
							if (fullsave) {
								if (reconcileIndexDirectory(null, MAIN_INDEX)) {
									if (MAIN_INDEX.isDirty())
										writeIndex(null, MAIN_INDEX);
								} else {
									// It was empty, just get rid of the index. The directories have already been cleared.
									MAIN_INDEX.markDead();
									MAIN_INDEX = null;
								}
							} else if (MAIN_INDEX.isDirty())
								writeIndex(null, MAIN_INDEX);
						}
						// Now do the project indexes. We have to walk all open projects to see which have an index. Since we are
						// doing a major save, the hit will ok
						IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects();
						for (int i=0; i<projects.length; i++) {
							project = projects[i];
							if (project.isOpen()) {
								Index index = (Index) project.getSessionProperty(PROJECT_INDEX_KEY);
								if (index != null) {
									if (fullsave) {
										if (reconcileIndexDirectory(project, index)) {
											if (index.isDirty())
												writeIndex(project, index);
										} else {
											// It was empty, just get rid of the index from memory. It has already been deleted from disk.
											index.markDead();
											project.setSessionProperty(PROJECT_INDEX_KEY, null);
										}
									} else if (index.isDirty())
										writeIndex(project, index);
								}
							}
						}
					}
			}
		}

		/*
		 * Write an index. Project if not null indicates a project index.
		 */
		private void writeIndex(IProject project, Index index) {
			ObjectOutputStream oos = null;
			try {
				File indexDirFile = getCacheDir(project).toFile();
				indexDirFile.mkdirs();
				File indexFile = new File(indexDirFile, INDEXFILENAME);
				oos = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(indexFile)));
				oos.writeObject(index);
				index.setDirty(false);
			} catch (IOException e) {
				BeaninfoPlugin.getPlugin().getLogger().log(e);
			} finally {
				if (oos != null)
					try {
						oos.close();
					} catch (IOException e) {
						BeaninfoPlugin.getPlugin().getLogger().log(e);
					}
			}
		}

		/*
		 * Reconcile the index directory of unused (empty) root directories. If after reconciling the index is empty, then it will delete the index file too. @return
		 * true if index not empty, false if index was empty and was erased too.
		 */
		private boolean reconcileIndexDirectory(IProject project, Index index) {
			// clean out unused rootIndexes
			File indexDir = getCacheDir(project).toFile();
			if (indexDir.canWrite()) {
				// Create a set of all root names for quick look up.
				if (index.rootToRootIndex.isEmpty()) {
					// It is empty, clear everything, including index file.
					cleanDirectory(indexDir, false);
					return false;
				} else {
					// Need a set of the valid rootnames for quick lookup of names in the directory list.
					// And while accumulating this list, clean out the root indexes cache too (i.e. the class cache files).
					final Set validFiles = new HashSet(index.rootToRootIndex.size());
					validFiles.add(INDEXFILENAME);
					for (Iterator itr = index.rootToRootIndex.values().iterator(); itr.hasNext();) {
						RootIndex rootIndex = (RootIndex) itr.next();
						if (reconcileClassCacheDirectory(rootIndex, project)) {
							// The class cache has been reconciled, and there are still some classes left, so keep the root index.
							validFiles.add(rootIndex.getRootName());
						} else {
							itr.remove(); // The root index is empty, so get rid of it. Since not a valid name, it will be deleted in next step.
							index.setDirty(true); // Also set it dirty in case it wasn't because we need to write out the container Index since it was
							// changed.
						}
					}
					// Get list of files and delete those that are not a valid name (used root name, or index file)
					String[] fileNames = indexDir.list();
					for (int i = 0; i < fileNames.length; i++) {
						if (!validFiles.contains(fileNames[i])) {
							File file = new File(indexDir, fileNames[i]);
							if (file.isDirectory())
								cleanDirectory(file, true);
							else
								file.delete();
						}
					}
					return true;
				} 
			} else 
				return true;	// Can't write, so treat as leave alone.
		}

		/*
		 * Reconcile the class cache directory for the root index. Return true if reconciled good but not empty. Return false if the class cache
		 * directory is now empty. In this case we should actually get rid of the entire root index. This makes sure that the directory matches
		 * the contents of the index by removing any file not found in the index.
		 */
		private boolean reconcileClassCacheDirectory(RootIndex rootIndex, IProject project) {
			if (rootIndex.classNameToClassEntry.isEmpty())
				return false; // There are no classes, so get rid the entire root index.
			else {
				final Set validFiles = rootIndex.classNameToClassEntry.keySet(); // The keys (classnames) are the filenames (without extension)
																					// that
				// should be kept.
				File indexDir = getCacheDir(project).append(rootIndex.getRootName()).toFile();
				// Get list of files that are not a valid name (used classname)
				String[] fileNames = indexDir.list();
				if (fileNames != null) {
					for (int i = 0; i < fileNames.length; i++) {
						String fileName = fileNames[i];
						if (fileName.endsWith(OVERRIDE_CACHE_SUFFIX)) {
							// Ends with out class cache extension, see if valid classname.
							String classname = fileName.substring(0, fileName.length() - OVERRIDE_CACHE_SUFFIX.length());
							ClassEntry ce = (ClassEntry) rootIndex.classNameToClassEntry.get(classname);
							if (ce != null && ce.overrideCacheExists())
								continue; // It is one of ours. Keep it.
						} else if (fileName.endsWith(CLASS_CACHE_SUFFIX)) {
							// Ends with out class cache extension, see if valid classname.
							if (validFiles.contains(fileName.substring(0, fileName.length() - CLASS_CACHE_SUFFIX.length()))) // Strip down to just
																																// class and see if
																																// one of ours.
								continue; // It is one of ours. Keep it.
						}
						// Not valid, get rid of it.
						File file = new File(indexDir, fileName);
						if (file.isDirectory())
							cleanDirectory(file, true);
						else
							file.delete();

					}
				}
				return true;
			}
		}
	}
	
	private static void cleanDirectory(File dir, boolean eraseDir) {
		if (dir.canWrite()) {
			File[] files = dir.listFiles();
			for (int i = 0; i < files.length; i++) {
				if (files[i].isDirectory())
					cleanDirectory(files[i], true);
				else
					files[i].delete();
			}
			if (eraseDir)
				dir.delete();
		}
	}	

	//-------------- Save Class Cache Entry Job -------------------
	// This is write queue for class caches. It is a FIFO queue. It is sychronized so that adds/removes are controlled.
	// Entries are ClassEntry's. The class entry has the resource that needs to be written out. It will be set to null
	// by the job when it is written. The job will have a ClassEntry locked while it is retrieving and resetting the resource
	// field in the entry.
	//
	// The process is the new cache will lock, create resource, set resource into the CE and release lock. Then add the CE to the queue
	// and schedule the job (in case job is not running).
	//
	// The job will lock the CE, get resource from the CE, write it out, set it back to null, release the CE). If the resource is null,
	// then it was already processed (this could happen if the job didn't get a chance to save it before another entry was posted
	// and this is the second request and it was actually processed by the first request).
	// IE:
	// 1) resource created, queue entry added
	// 2) 2nd req, job not processed yet, resource recreated and put back into CE, new queue entry.
	// 3) job pulls from queue, locks ce, grabs resource, writes out the resource, sets back to null, release ce.
	// 4) job pulls from queue. This time the resoure is null so it skips it.
	//
	// Need to lock Ce during entire create and write because the resource set is not reentrant so can't be writing it while creating it.

	private List cacheWriteQueue = null;

	void waitForCacheSaveJob() {
		// For a full save we want to get the class cache files written too, so we need to manipulate the job to get it to finish ASAP.
		if (cacheWriteJob != null) {
			if (BeaninfoPlugin.getPlugin().getLogger().isLoggingLevel(Level.FINER))
				BeaninfoPlugin.getPlugin().getLogger().log("Forcing a cache save job to start early.", Level.FINER); //$NON-NLS-1$
			switch (cacheWriteJob.getState()) {
				case Job.SLEEPING:
					// It could be waiting a long time, so we need to wake it up at a high priority to get it running ASAP.
					cacheWriteJob.setPriority(Job.INTERACTIVE); // Need to get it going right away
					cacheWriteJob.wakeUp();
					// Now drop into the wait.
				default:
					// Now wait for it (if not running this will return right away).
					try {
						cacheWriteJob.join();
					} catch (InterruptedException e) {
					}
			}
		}
	}

	static final Map SAVE_CACHE_OPTIONS;
	static {
		SAVE_CACHE_OPTIONS = new HashMap(3);
		SAVE_CACHE_OPTIONS.put(XMLResource.OPTION_SAVE_TYPE_INFORMATION, Boolean.TRUE);
		SAVE_CACHE_OPTIONS.put(XMLResource.OPTION_USE_ENCODED_ATTRIBUTE_STYLE, Boolean.TRUE);
        SAVE_CACHE_OPTIONS.put(XMLResource.OPTION_ENCODING, "UTF-8"); //$NON-NLS-1$
	}

	protected Job cacheWriteJob = null;
	protected Adapter projectReleaseAdapter = new AdapterImpl() {
		
		/* (non-Javadoc)
		 * @see org.eclipse.emf.common.notify.impl.AdapterImpl#isAdapterForType(java.lang.Object)
		 */
		public boolean isAdapterForType(Object type) {
			return type == BeanInfoCacheController.this;	// We're making the BeanInfoCacheController.this be the adapter type.
		}
		
		
		/* (non-Javadoc)
		 * @see org.eclipse.emf.common.notify.impl.AdapterImpl#notifyChanged(org.eclipse.emf.common.notify.Notification)
		 */
		public void notifyChanged(Notification msg) {
			if (msg.getEventType() == ProjectResourceSet.SPECIAL_NOTIFICATION_TYPE && msg.getFeatureID(BeanInfoCacheController.class) == ProjectResourceSet.PROJECTRESOURCESET_ABOUT_TO_RELEASE_ID) {
				// This is an about to be closed. If we have an active write job, bring it up to top priority and wait for it to finish.
				// This will make sure any resources in the project are written. There may not be any waiting, but this is doing a close
				// project, which is slow already relatively speaking, that waiting for the cache write job to finish is not bad.
				waitForCacheSaveJob();
			}
		}
	};

	private void queueClassEntry(ClassEntry ce, ResourceSet rset) {
		if (cacheWriteQueue == null) {
			cacheWriteQueue = Collections.synchronizedList(new LinkedList());
			cacheWriteJob = new Job(BeaninfoCoreMessages.BeanInfoCacheController_Job_WriteBeaninfoCache_Title) { 

				protected IStatus run(IProgressMonitor monitor) {
					monitor.beginTask("", cacheWriteQueue.size() + 10); // This is actually can change during the run, so we add 10 for the heck of it. //$NON-NLS-1$
					if (BeaninfoPlugin.getPlugin().getLogger().isLoggingLevel(Level.FINER))
						BeaninfoPlugin.getPlugin().getLogger().log("Starting write BeanInfo Cache files.", Level.FINER); //$NON-NLS-1$
					while (!monitor.isCanceled() && !cacheWriteQueue.isEmpty()) {
						ClassEntry ce = (ClassEntry) cacheWriteQueue.remove(0); // Get first one.
						boolean dead = false;
						synchronized (BeanInfoCacheController.this) {
							if (ce.getRootIndex().getIndex().isDead()) {
								dead = true;	// The index is dead, so don't write it. We still need to go through and get the pending resource out of its resource set so that it goes away.
							}
						}
						synchronized (ce) {
							Resource cres = ce.getPendingResource();
							if (cres != null) {
								try {
									if (!dead)
										cres.save(SAVE_CACHE_OPTIONS);
								} catch (IOException e) {
									BeaninfoPlugin.getPlugin().getLogger().log(e);
								} finally {
									// Remove the resource from resource set, clear out the pending.
									cres.getResourceSet().getResources().remove(cres);
									ce.setPendingResource(null);
								}
							}
							cres = ce.getPendingOverrideResource();
							if (cres != null) {
								try {
									if (!dead)
										cres.save(SAVE_CACHE_OPTIONS);
								} catch (IOException e) {
									BeaninfoPlugin.getPlugin().getLogger().log(e);
								} finally {
									// Remove the resource from resource set, clear out the pending.
									cres.getResourceSet().getResources().remove(cres);
									ce.setPendingOverrideResource(null);
								}
							}
							
							monitor.worked(1);
						}
					}
					monitor.done();
					if (BeaninfoPlugin.getPlugin().getLogger().isLoggingLevel(Level.FINER))
						BeaninfoPlugin.getPlugin().getLogger().log("Finished write BeanInfo Cache files.", Level.FINER); //$NON-NLS-1$
					return Status.OK_STATUS;
				}
			};
			cacheWriteJob.setPriority(Job.SHORT);
			cacheWriteJob.setSystem(true);
		}
		if (rset != null && EcoreUtil.getExistingAdapter(rset, this) == null) {
			// If it is a project resource set, then add ourselves as listeners so we know when released.
			if (rset instanceof ProjectResourceSet)
				rset.eAdapters().add(projectReleaseAdapter);
		}
		cacheWriteQueue.add(ce);
		cacheWriteJob.schedule(60 * 1000L); // Put off for 1 minute to let other stuff go on. Not important that it happens immediately.
	}
}

Back to the top