core/com.nokia.carbide.cpp.sdk.core/src/com/nokia/carbide/cpp/internal/api/sdk/SymbianBuildContextDataCache.java
author timkelly
Fri, 12 Feb 2010 09:56:50 -0600
changeset 945 67437bfc7c6f
parent 840 c92830672f75
child 1434 79471fd1fd69
permissions -rw-r--r--
Add ISymbianManagerLoadedHook - currently used to notify com.nokia.qt plugins that need to ensure that Qt SDKs have been scanned and added to the Qt preferences and that ICarbideBuildConfiguration listeners are added so the proper Qt-SDK can be set with build config changes. Scanned Qt SDKs are wrapped in a Job and added SDKs are reported to the Error log as Info.

/*
* Copyright (c) 2009 Nokia Corporation and/or its subsidiary(-ies). 
* All rights reserved.
* This component and the accompanying materials are made available
* under the terms of the License "Eclipse Public License v1.0"
* which accompanies this distribution, and is available
* at the URL "http://www.eclipse.org/legal/epl-v10.html".
*
* Initial Contributors:
* Nokia Corporation - initial contribution.
*
*/
package com.nokia.carbide.cpp.internal.api.sdk;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.io.ObjectStreamException;
import java.util.*;

import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;

import com.nokia.carbide.cpp.epoc.engine.EpocEnginePlugin;
import com.nokia.carbide.cpp.epoc.engine.model.sbv.ISBVView;
import com.nokia.carbide.cpp.epoc.engine.preprocessor.*;
import com.nokia.carbide.cpp.internal.sdk.core.model.SymbianSDK;
import com.nokia.carbide.cpp.sdk.core.*;
import com.nokia.carbide.internal.api.cpp.epoc.engine.preprocessor.BasicIncludeFileLocator;
import com.nokia.carbide.internal.api.cpp.epoc.engine.preprocessor.MacroScanner;
import com.nokia.cpp.internal.api.utils.core.ExternalFileInfoCollection;
import com.nokia.cpp.internal.api.utils.core.FileUtils;
import com.nokia.cpp.internal.api.utils.core.Logging;
import com.nokia.cpp.internal.api.utils.core.ObjectUtils;

/**
 * This class holds the externally gathered data for a build context,
 * such as the #include paths and macros defined in the SDK.
 * <p> 
 * Unlike CarbideBuildConfiguration (which, unfortunately extends 
 * SymbianBuildContext), this will not be created multiple times for
 * multiple projects, but only once for a different build context
 * in an SDK or devkit.
 */
public class SymbianBuildContextDataCache {

	public static boolean DEBUG = false;
	
	// by default, only check HRH-included files if changed in last second (for ordinary operations)
	// or the last minute (when doing a long project operation).  see #startThrottle() and #stopThrottle()
	private static final long DEFAULT_HRH_INFO_CHECK_QUANTUM = 1000; // 1 sec
	private static final long THROTTLED_HRH_INFO_CHECK_QUANTUM = 60000; // 60 sec

	// compiler prefixes are very unlikely to change, but we need to check
	// occasionally in case a user installs a new one...
	private static final long DEFAULT_COMPILER_PREFIX_INFO_CHECK_QUANTUM = 15 * 60 * 1000;	// 15 minutes
	
	// This is a count of times #startProjectOperation() was called without
	// balancing #endProjectOperation().  
	private static int inProjectOperationCount;
	
	private static Map<String, SymbianBuildContextDataCache> cacheMap = new HashMap<String, SymbianBuildContextDataCache>();
	
	public static synchronized SymbianBuildContextDataCache getCache(ISymbianBuildContext context) {
		// don't hash on ISymbianBuildContext itself since it is sometimes a ICarbideBuildConfiguration
		String key = getBuildContextKey(context);
		
		SymbianBuildContextDataCache cache = cacheMap.get(key);
		if (cache == null) {
			cache = new SymbianBuildContextDataCache(context);
			cache.loadCacheFile();
			cacheMap.put(key, cache);
		}
		return cache;
	}

	/**
	 * @param context
	 * @return
	 */
	private static String getBuildContextKey(ISymbianBuildContext context) {
		String key = context.getPlatformString() + "/" + context.getTargetString() + "/";
		ISymbianSDK sdk = context.getSDK();
		if (sdk != null)
			key += sdk.getEPOCROOT();
		return key;
	}

	//private File prefixFileParsed;
	private List<File> hrhFilesParsed = new ArrayList<File>();
	private ExternalFileInfoCollection hrhFileInfo = null; 
	private List<IDefine> variantHRHMacros = new ArrayList<IDefine>();
	private List<IDefine> compilerPrefixMacros = new ArrayList<IDefine>();
	private ExternalFileInfoCollection compilerPrefixFileInfo = null; 
	private List<File> systemIncludes;
	private ISymbianSDK sdk;
	private IPath compilerPrefixFile;

	private String platformString;

	private String displayString;

	private String contextKey;
	
	private boolean changed;

	private File cacheFile;

	private SymbianBuildContextDataCache(ISymbianBuildContext context) {
		if (DEBUG) System.out.println("Creating cache for " + context.getDisplayString());
		this.platformString = context.getPlatformString();
		this.displayString = context.getDisplayString();
		this.sdk = context.getSDK();
		this.contextKey = getBuildContextKey(context);
	}

	/* (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return "Cache for " + displayString;
	}
	
	public List<IDefine> getVariantHRHDefines() {

		// we parse the variant hrh file to gather macros.  this can be time consuming so do it
		// once and cache the values.  only reset the cache when the hrh or any of its includes
		// has changed.
		
		boolean buildCache = false;
		
		if (hrhFileInfo == null) {
			// hasn't been built yet, or was flushed
			buildCache = true;
		} else {
			// Cache exists.  See if any of the files have changed
			if (sdk != null && hrhFileInfo.anyChanged()) {
				buildCache = true;
			}
		}
		
		if (buildCache) {
			gatherVariantHRHDefines();
		}
			
		return variantHRHMacros;
	}

	/**
	 * Re-gather the #defines from the variant HRH file 
	 */
	private void gatherVariantHRHDefines() {
		changed = true;
		variantHRHMacros.clear();
		
		synchronized (this) {

			List<IDefine> macros = new ArrayList<IDefine>();
			Map<String, IDefine> namedMacros = new HashMap<String, IDefine>();
			File prefixFile = sdk.getPrefixFile();
			
			if (prefixFile == null){
				// Check that the prefix file may have become available since the SDK was scanned last.
				// This can happen, for e.g., if the user opens the IDE _then_ does a subst on a drive that already has an SDK entry.
				IPath prefixCheck = ((SymbianSDK)sdk).getPrefixFromVariantCfg();
				if (prefixCheck != null){
					prefixFile = prefixCheck.toFile();
					sdk.setPrefixFile(prefixCheck);
				}
			}
			
			File[] includedFiles = null;

			if (prefixFile != null) {
				
				List<File> systemPaths = new ArrayList<File>();
				// Always add epoc32/include to the search path as this is implicit for includes in the HRH
				systemPaths.add(new File(sdk.getEPOCROOT() + "epoc32/include"));
				
				// add any BSF/SBV includes so the headers are picked up from the correct location
				IBSFPlatform bsfPlat = sdk.getBSFCatalog().findPlatform(platformString);
				ISBVPlatform sbvPlat = sdk.getSBVCatalog().findPlatform(platformString);
				if (bsfPlat != null) {
					for (IPath path : bsfPlat.getSystemIncludePaths()) {
						systemPaths.add(path.toFile());
					}
				} else if (sbvPlat != null) {
					LinkedHashMap<IPath, String> platPaths = sbvPlat.getBuildIncludePaths();
					Set<IPath> set = platPaths.keySet();
					for (IPath path : set) {
						String pathType = platPaths.get(path);
						if (pathType.equalsIgnoreCase(ISBVView.INCLUDE_FLAG_PREPEND) || pathType.equalsIgnoreCase(ISBVView.INCLUDE_FLAG_SET)){
							systemPaths.add(path.toFile());
						}
					}
				}
				
				MacroScanner scanner = new MacroScanner(
						new BasicIncludeFileLocator(null, systemPaths.toArray(new File[systemPaths.size()])),
						DefaultModelDocumentProvider.getInstance(),
						DefaultTranslationUnitProvider.getInstance());
				scanner.scanFile(prefixFile);

				List<IDefine> scannedMacros = (List<IDefine>)scanner.getMacroDefinitions();
				for (IDefine scannedMacro : scannedMacros){
					// we don't want duplicate macros, so check to see if it's already there.
					// if it is, remove it and then add the newer one.  this is consistent with
					// how it would be from a compiler standpoint.
					IDefine macro = namedMacros.get(scannedMacro.getName());
					if (macro != null) {
						macros.remove(macro);
					}
					
					macros.add(scannedMacro);
					namedMacros.put(scannedMacro.getName(), scannedMacro);
				}
				
				hrhFilesParsed.clear();
				includedFiles = scanner.getIncludedFiles();
				for (File inc : includedFiles) {
					hrhFilesParsed.add(inc);
				}
				
				List<String> variantCFGMacros = new ArrayList<String>();
				variantCFGMacros = sdk.getVariantCFGMacros();
				for (String cfgMacros : variantCFGMacros){
					// we don't want duplicate macros, so check to see if it's already there.
					IDefine existingMacro = namedMacros.get(cfgMacros);
					if (existingMacro != null) {
						macros.remove(existingMacro);
					}
					
					IDefine macro = DefineFactory.createSimpleFreeformDefine(cfgMacros);
					macros.add(macro);
					namedMacros.put(macro.getName(), macro);
				}
			} 
			
			// cache the info about when we created the cache
			if (hrhFileInfo == null) {
				hrhFileInfo = new ExternalFileInfoCollection(
						EpocEnginePlugin.getExternalFileInfoCache(),
						includedFiles,
						DEFAULT_HRH_INFO_CHECK_QUANTUM);
			} else {
				hrhFileInfo.setFiles(includedFiles);
			}

			variantHRHMacros = macros;
			saveCacheFile();
		}
	}

	public List<File> getPrefixFileIncludes() {
		return hrhFilesParsed;
	}


	public synchronized List<IDefine> getCompilerMacros(IPath prefixFile) {
		
		// we assume that the prefix file will not change often,
		// (if at all) for a build context, so dump the cache if the prefix file changes.
		
		if (compilerPrefixFile != null && !compilerPrefixFile.equals(prefixFile)) {
			compilerPrefixFileInfo = null;
		}
		
		compilerPrefixFile = prefixFile;

		if (compilerPrefixFileInfo == null ||
				compilerPrefixFileInfo.anyChanged()) {

			changed = true;

			compilerPrefixMacros.clear();
			
			synchronized (this) {

				List<IDefine> macros = new ArrayList<IDefine>();
				if (prefixFile != null) {

					List<File> userPaths = new ArrayList<File>();
					List<File> systemPaths = new ArrayList<File>();
					
					userPaths.add(prefixFile.removeLastSegments(1).toFile());
					systemPaths.add(prefixFile.removeLastSegments(1).toFile());
					IPath includePath = sdk.getIncludePath();
					if (includePath != null) {
						File includeDir = includePath.toFile().getAbsoluteFile();
						userPaths.add(includeDir);
						systemPaths.add(includeDir);
					}

					
					// get macros from the compiler prefix file: note, this is a stupid
					// scan that will get the last version #defined, even if inside an #if.
					MacroScanner scanner = new MacroScanner(
							new BasicIncludeFileLocator(userPaths.toArray(new File[userPaths.size()]), systemPaths.toArray(new File[systemPaths.size()])),
							DefaultModelDocumentProvider.getInstance(), 
							DefaultTranslationUnitProvider.getInstance());
					scanner.scanFile(prefixFile.toFile());
					for (IDefine define : scanner.getMacroDefinitions()) {
						macros.add(define);
					}

					// store off the info about what we read for this cache
					File[] files = scanner.getIncludedFiles();
					
					if (compilerPrefixFileInfo == null)
						compilerPrefixFileInfo = new ExternalFileInfoCollection(
								EpocEnginePlugin.getExternalFileInfoCache(),
								files,
								DEFAULT_COMPILER_PREFIX_INFO_CHECK_QUANTUM);
					else
						compilerPrefixFileInfo.setFiles(files);
				}
				
				compilerPrefixMacros = macros;
				
				saveCacheFile();
			}
		}
			
		return compilerPrefixMacros;
	}

	/**
	 * Get the list of #include paths detected for this context.
	 * @return List or <code>null</code>
	 */
	public synchronized List<File> getSystemIncludePaths() {
		if (systemIncludes == null) {
			gatherBuildContextSystemIncludePaths();
		}
		return systemIncludes;
	}

	/**
	 * Fetch the list of include paths for the build context
	 */
	private void gatherBuildContextSystemIncludePaths() {
		changed = true;
		
		systemIncludes = new ArrayList<File>();
		
		if (DEBUG) System.out.println("Scanning include paths for " + displayString);
		
		IBSFPlatform bsfplatform = sdk.getBSFCatalog().findPlatform(platformString);
		ISBVPlatform sbvPlatform = sdk.getSBVCatalog().findPlatform(platformString);

		// look in the epoc32 directory of the SDK
		IPath includePath = sdk.getIncludePath();
		if (includePath != null) {
			File includeDir = includePath.toFile().getAbsoluteFile();
			File dir;
			
			// get additional include directories from BSF platform, if defined
			if (bsfplatform != null) {
				IPath[] systemIncludePaths = bsfplatform.getSystemIncludePaths();
				for (IPath path : systemIncludePaths) {
					dir = path.toFile();
					if (dir.exists() && dir.isDirectory()) {
						systemIncludes.add(dir);
					}
				}
			} else if (sbvPlatform != null){
				
				LinkedHashMap<IPath, String> platPaths = sbvPlatform.getBuildIncludePaths();
				Set<IPath> set = platPaths.keySet();
				for (IPath path : set) {
					String pathType = platPaths.get(path);
					if (pathType.equalsIgnoreCase(ISBVView.INCLUDE_FLAG_PREPEND) || pathType.equalsIgnoreCase(ISBVView.INCLUDE_FLAG_SET)){
						dir = path.toFile();
						systemIncludes.add(dir);
					}
				}
			}
			else {
				// legacy behavior 
				if (platformString.equals(ISymbianBuildContext.EMULATOR_PLATFORM)) {
					dir = new File(includeDir, "wins"); //$NON-NLS-1$
					if (dir.exists() && dir.isDirectory()) {
						systemIncludes.add(dir);
					}
				}
			}

			// add OEM dir
			dir = new File(includeDir, "oem"); //$NON-NLS-1$
			if (dir.exists() && dir.isDirectory()) {
				systemIncludes.add(dir);
			}

			// and finally the normal include dir
			systemIncludes.add(includeDir);
			
			// and finally, finally, if this is an SBV add any paths with the append flag
			if (sbvPlatform != null){
				
				Map<IPath, String> platPaths = sbvPlatform.getBuildIncludePaths();
				Set<IPath> set = platPaths.keySet();
				for (IPath path : set) {
					String pathType = platPaths.get(path);
					if (pathType.equalsIgnoreCase(ISBVView.INCLUDE_FLAG_APPEND)){
						dir = path.toFile();
						systemIncludes.add(dir);
					}
				}	
			}
		}
		
		// also search files in same folder as variant.hrh
		File prefix = sdk.getPrefixFile();
		if (sbvPlatform != null){
			// might be an alternate HRH file to use
			IPath varVarHRH = sbvPlatform.getBuildVariantHRHFile();
			if (!varVarHRH.toFile().equals(prefix) && varVarHRH.toFile().exists()){
				prefix = varVarHRH.toFile();
			} 
		}
		if (prefix != null) {
			systemIncludes.add(prefix.getParentFile());
		}
		
		saveCacheFile();
	}

	/**
	 * Let cache know that the client is beginning an operation that
	 * will iterate all the build contexts.  We use this information
	 * to optimize file info checks.
	 * <p>
	 * Each call must be balanced by an {@link #endProjectOperation()} call.
	 * Use a try ... finally block to make sure.
	 */
	public static synchronized void startProjectOperation() {
		inProjectOperationCount++;
		if (inProjectOperationCount == 1) {
			for (SymbianBuildContextDataCache cache : cacheMap.values()) {
				cache.startThrottle();
			}
		}
	}

	/**
	 * Let cache know that a project-wide operation is done, so
	 * we can resume normal info checking behavior.
	 */
	public static synchronized void endProjectOperation() {
		inProjectOperationCount--;
		if (inProjectOperationCount < 0) {
			Logging.log(SDKCorePlugin.getDefault(), 
					Logging.newStatus(SDKCorePlugin.getDefault(), 
							new IllegalStateException("project operation count not balanced")));
			inProjectOperationCount = 0;
		}
		if (inProjectOperationCount == 0) {
			for (SymbianBuildContextDataCache cache : cacheMap.values()) {
				cache.stopThrottle();
				cache.saveCacheFile();
			}
		}
	}

	/**
	 * Throttle file info checks on this cache if we suspect the same
	 * files will be checked over and over again in a short time (e.g. through
	 * multiple contexts on the same SDK).
	 */
	private void startThrottle() {
		if (hrhFileInfo != null) {
			hrhFileInfo.setRecheckQuantum(THROTTLED_HRH_INFO_CHECK_QUANTUM);
		}
		// note: compiler prefix infos already have a long delay, but
		// this is a good place to refresh
		if (compilerPrefixFileInfo != null) {
			compilerPrefixFileInfo.refresh();
		}
	}

	/**
	 * End file info throttling.
	 */
	private void stopThrottle() {
		if (hrhFileInfo != null)
			hrhFileInfo.setRecheckQuantum(DEFAULT_HRH_INFO_CHECK_QUANTUM);
		// note: compiler prefix infos already have a long delay
	}

	/**
	 * Refresh the cached data when there are substantial changes in the given SDKs.
	 * 
	 * @param sdks SDKs for whose contexts the caches should be removed, or <code>null</code> for all of them
	 */
	public static synchronized void refreshForSDKs(ISymbianSDK[] sdks) {
		// refresh each context cache, meaning, delete its memory and disk values
		Collection<String> values = cacheMap.keySet();
		String[] keyArray = (String[]) values.toArray(new String[values.size()]);
		for (String key : keyArray) {
			SymbianBuildContextDataCache cache = cacheMap.get(key);
			boolean forSDK = sdkInArray(sdks, cache.sdk);
			if (forSDK) {
				cache.reset();
				cacheMap.remove(key);
			}
		}
	}

	private static boolean sdkInArray(ISymbianSDK[] sdks, ISymbianSDK anSDK) {
		if (sdks == null)
			return true;
		if (anSDK == null)
			return false;
		// object identity is not appropriate; user may have renamed or moved an SDK
		for (ISymbianSDK sdk : sdks) {
			if (ObjectUtils.equals(sdk.getEPOCROOT(), anSDK.getEPOCROOT())
					|| ObjectUtils.equals(sdk.getUniqueId(), anSDK.getUniqueId()))
				return true;
		}
		return false;
	}
	
	/**
	 * Reset the cached data for this context, ensuring it will be
	 * freshly gathered.  This deletes anything stored on disk.
	 */
	public synchronized void reset() {
		hrhFileInfo = null;
		compilerPrefixFileInfo = null;
		systemIncludes = null;
		getCacheFile().delete();
	}

	/**
	 * Get the file where we store cached data
	 * @return File in workspace plugin state storage
	 */
	protected File getCacheFile() {
		if (cacheFile == null) {
			IPath statePath;
			if (Platform.isRunning())
				statePath = Platform.getStateLocation(SDKCorePlugin.getDefault().getBundle());
			else
				statePath = new Path(FileUtils.getTemporaryDirectory().getAbsolutePath());
			cacheFile = statePath.append(contextKey.replaceAll("[^A-Za-z0-9_]", "_") + ".dat").toFile(); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		}
		return cacheFile;
	}
	
	/**
	 * Save cache to disk if changed.
	 */
	protected void saveCacheFile() {
		if (!changed)
			return;
			
		ObjectOutputStream os = null;
		try {
			File cacheFile = getCacheFile();
			if (DEBUG) System.out.print("Saving to " + cacheFile + "... "); //$NON-NLS-1$ //$NON-NLS-2$
			os = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(cacheFile)));
			doSaveCache(os);
			changed = false;
			if (DEBUG) System.out.println("done."); //$NON-NLS-1$
		} catch (ObjectStreamException e) {
			Logging.log(SDKCorePlugin.getDefault(), 
					Logging.newStatus(SDKCorePlugin.getDefault(), 
							IStatus.WARNING,
							"Tried to save uncacheable data for " + displayString, //$NON-NLS-1$
							e));
		} catch (IOException e) {
			// oh well
			Logging.log(SDKCorePlugin.getDefault(), 
					Logging.newStatus(SDKCorePlugin.getDefault(), 
							IStatus.WARNING,
							"Failed to save cache state for " + displayString, //$NON-NLS-1$
							e));
		} finally {
			if (os != null) {
				try {
					os.close();
				} catch (IOException e) {
				}
			}
		}
	}
	
	protected void loadCacheFile() {
		ObjectInputStream is = null;
		// Use the class loader that knows how to span plugins
		final ClassLoader classLoader = SDKCorePlugin.getDefault().getClass().getClassLoader();
		File cacheFile = getCacheFile();
		try {
			is = new ObjectInputStream(new BufferedInputStream(new FileInputStream(cacheFile))) {
				/* (non-Javadoc)
				 * @see java.io.ObjectInputStream#resolveClass(java.io.ObjectStreamClass)
				 */
				@Override
				protected Class<?> resolveClass(ObjectStreamClass desc)
						throws IOException, ClassNotFoundException {
					String name = desc.getName();
					try {
						return classLoader.loadClass(name);
					} catch (ClassNotFoundException e) {
						return super.resolveClass(desc);
					}
				}
			};
			doLoadCache(is);
			if (DEBUG) System.out.println("Loaded cache from " + cacheFile); //$NON-NLS-1$
		} catch (ClassNotFoundException e) {
			// this is probably a bug
			e.printStackTrace();
		} catch (ObjectStreamException e) {
			// assume it's just out of sync (e.g. object signatures changed), so nothing in Error Log
			e.printStackTrace();
			cacheFile.delete();
		} catch (IOException e) {
			// oh well, not made yet
		} finally {
			if (is != null) {
				try {
					is.close();
				} catch (IOException e) {
				}
			}
			changed = false;
		}
	}

	/**
	 * @param os
	 */
	private void doSaveCache(ObjectOutputStream os) throws IOException {
		os.writeObject(systemIncludes);
		savePath(os, compilerPrefixFile);
		os.writeObject(compilerPrefixFileInfo);
		os.writeObject(compilerPrefixMacros);
		os.writeObject(hrhFileInfo);
		os.writeObject(hrhFilesParsed);
		os.writeObject(variantHRHMacros);
	}

	
	/**
	 * Load entries from the cache.  This must match the ordering in
	 * {@link #doSaveCache(ObjectOutputStream)}.
	 * @param is
	 */
	private void doLoadCache(ObjectInputStream is) throws IOException, ClassCastException, ClassNotFoundException {
		systemIncludes = loadList(is, File.class);
		compilerPrefixFile = loadPath(is);
		compilerPrefixFileInfo = (ExternalFileInfoCollection) is.readObject();
		compilerPrefixMacros = loadList(is, IDefine.class);
		hrhFileInfo = (ExternalFileInfoCollection) is.readObject();
		hrhFilesParsed = loadList(is, File.class);
		variantHRHMacros = loadList(is, IDefine.class);
	}

	/**
	 * Read an IPath from a portable string
	 * @param is
	 * @return an IPath constructed from a String
	 */
	private IPath loadPath(ObjectInputStream is) throws IOException, ClassCastException, ClassNotFoundException {	
		String path;
		path = (String) is.readObject();
		if (path != null)
			return Path.fromPortableString(path);
		return null;
	}
	
	/**
	 * Save an IPath as a portable string
	 * @param os
	 * @param path
	 * @throws IOException
	 */
	private void savePath(ObjectOutputStream os, IPath path) throws IOException {
		os.writeObject(path != null ? path.toPortableString() : null);
	}

	/**
	 * Load a list from the object stream, throwing if it appears to contain
	 * the wrong kind of data.
	 * @param is
	 * @param klass
	 * @return a List or <code>null</code>
	 */
	@SuppressWarnings("unchecked")
	private <T> List<T> loadList(ObjectInputStream is, Class<T> klass) throws IOException, ClassCastException, ClassNotFoundException { 
		List<T> list = (List) is.readObject();
		if (list == null || klass == null || list.size() == 0)
			return list;
		if (!klass.isInstance(list.get(0)))
			throw new IOException("Class contains " + list.get(0).getClass() + ", not " + klass); //$NON-NLS-1$ //$NON-NLS-2$
		return list;
	}
}