alvinalexander.com | career | drupal | java | mac | mysql | perl | scala | uml | unix  

What this is

This file is included in the DevDaily.com "Java Source Code Warehouse" project. The intent of this project is to help you "Learn Java by Example" TM.

Other links

The source code

/*******************************************************************************
 * Copyright (c) 2007, 2008 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
 *******************************************************************************/
package org.eclipse.pde.api.tools.internal;

import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.ISaveContext;
import org.eclipse.core.resources.ISaveParticipant;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.jdt.core.ElementChangedEvent;
import org.eclipse.jdt.core.IElementChangedListener;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaElementDelta;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.pde.api.tools.internal.builder.ApiAnalysisBuilder;
import org.eclipse.pde.api.tools.internal.provisional.ApiPlugin;
import org.eclipse.pde.api.tools.internal.provisional.Factory;
import org.eclipse.pde.api.tools.internal.provisional.IApiComponent;
import org.eclipse.pde.api.tools.internal.provisional.IApiProfile;
import org.eclipse.pde.api.tools.internal.provisional.IApiProfileManager;
import org.eclipse.pde.api.tools.internal.util.Util;
import org.eclipse.pde.core.plugin.IPluginModelBase;
import org.eclipse.pde.core.plugin.ModelEntry;
import org.eclipse.pde.core.plugin.PluginRegistry;
import org.eclipse.pde.internal.core.IPluginModelListener;
import org.eclipse.pde.internal.core.PDECore;
import org.eclipse.pde.internal.core.PluginModelDelta;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * This manager is used to maintain (persist, restore, access, update) Api profiles.
 * This manager is lazy, in that caches are built and maintained when requests
 * are made for information, nothing is pre-loaded when the manager is initialized.
 * 
 * @since 1.0.0
 */
public final class ApiProfileManager implements IApiProfileManager, ISaveParticipant, IElementChangedListener, IPluginModelListener, IResourceChangeListener {
	
	/**
	 * Constant used for controlling tracing in the API tool builder
	 */
	private static boolean DEBUG = Util.DEBUG;
	
	/**
	 * Method used for initializing tracing in the API tool builder
	 */
	public static void setDebug(boolean debugValue) {
		DEBUG = debugValue || Util.DEBUG;
	}
	
	/**
	 * Constant for the default API profile.
	 * Value is: <code>default_api_profile
	 */
	private static final String DEFAULT_PROFILE = "default_api_profile"; //$NON-NLS-1$
	
	/**
	 * The main cache for the manager.
	 * The form of the cache is: 
	 * <pre>
	 * HashMap<String(profileid), ApiProfile>
	 * </pre>
	 */
	private HashMap profilecache = null;
	
	/**
	 * The current default {@link IApiProfile}
	 */
	private String defaultprofile = null;
	
	/**
	 * The current workspace profile
	 */
	private IApiProfile workspaceprofile = null;
	
	/**
	 * The default save location for persisting the cache from this manager.
	 */
	private IPath savelocation = ApiPlugin.getDefault().getStateLocation().append(".api_profiles").addTrailingSeparator(); //$NON-NLS-1$
	
	/**
	 * If the cache of profiles needs to be saved or not.
	 */
	private boolean fNeedsSaving = false;
	
	/**
	 * Constructor
	 */
	public ApiProfileManager() {
		ApiPlugin.getDefault().addSaveParticipant(this);
		JavaCore.addElementChangedListener(this, ElementChangedEvent.POST_CHANGE);
		ResourcesPlugin.getWorkspace().addResourceChangeListener(this, IResourceChangeEvent.POST_BUILD);
		PDECore.getDefault().getModelManager().addPluginModelListener(this);
	}
	
	/* (non-Javadoc)
	 * @see org.eclipse.pde.api.tools.IApiProfileManager#getApiProfile(java.lang.String)
	 */
	public synchronized IApiProfile getApiProfile(String name) {
		initializeStateCache();
		return (ApiProfile) profilecache.get(name);
	}

	/* (non-Javadoc)
	 * @see org.eclipse.pde.api.tools.IApiProfileManager#getApiProfiles()
	 */
	public synchronized IApiProfile[] getApiProfiles() {
		initializeStateCache();
		return (IApiProfile[]) profilecache.values().toArray(new IApiProfile[profilecache.size()]);
	}
	
	/* (non-Javadoc)
	 * @see org.eclipse.pde.api.tools.IApiProfileManager#addApiProfile(org.eclipse.pde.api.tools.model.component.IApiProfile)
	 */
	public synchronized void addApiProfile(IApiProfile newprofile) {
		if(newprofile != null) {
			initializeStateCache();
			profilecache.put(newprofile.getName(), newprofile);
			fNeedsSaving = true;
		}
	}
	
	/* (non-Javadoc)
	 * @see org.eclipse.pde.api.tools.IApiProfileManager#removeApiProfile(java.lang.String)
	 */
	public synchronized boolean removeApiProfile(String name) {
		if(name != null) {
			initializeStateCache();
			IApiProfile profile = (IApiProfile) profilecache.remove(name);
			if(profile != null) {
				profile.dispose();
				boolean success = true;
				//remove from filesystem
				File file = savelocation.append(name+".profile").toFile(); //$NON-NLS-1$
				if(file.exists()) {
					success &= file.delete();
				}
				fNeedsSaving = true;
				return success;
			}
		}
		return false;
	}
	
	/**
	 * Initializes the profile cache lazily. Only performs work
	 * if the current cache has not been created yet
	 * @throws FactoryConfigurationError 
	 * @throws ParserConfigurationException 
	 */
	private synchronized void initializeStateCache() {
		long time = System.currentTimeMillis();
		if(profilecache == null) {
			profilecache = new HashMap();
			File[] profiles = savelocation.toFile().listFiles(new FileFilter() {
				public boolean accept(File pathname) {
					return pathname.getName().endsWith(".profile"); //$NON-NLS-1$
				}
			});
			if(profiles != null) {
				InputStream fin = null;
				IApiProfile newprofile = null;
				for(int i = 0; i < profiles.length; i++) {
					File profile = profiles[i];
					if(profile.exists()) {
						try {
							fin = new FileInputStream(profile);
							newprofile = restoreProfile(fin);
							profilecache.put(newprofile.getName(), newprofile);
						}
						catch (IOException e) {
							ApiPlugin.log(e);
						}
						catch(CoreException e) {
							ApiPlugin.log(e.getStatus());
						}
					}
				}
			}
			String def = ApiPlugin.getDefault().getPluginPreferences().getString(DEFAULT_PROFILE);
			IApiProfile profile = (IApiProfile) profilecache.get(def);
			defaultprofile = (profile != null ? def : null);
			if(DEBUG) {
				System.out.println("Time to initialize state cache: " + (System.currentTimeMillis() - time) + "ms"); //$NON-NLS-1$ //$NON-NLS-2$
			}
		}
	}
	
	/**
	 * Persists all of the cached elements to individual xml files named 
	 * with the id of the API profile
	 * @throws IOException 
	 */
	private void persistStateCache() throws CoreException, IOException {
		if(defaultprofile != null) {
			ApiPlugin.getDefault().getPluginPreferences().setValue(DEFAULT_PROFILE, defaultprofile);
		}
		if(profilecache != null) {
			File dir = new File(savelocation.toOSString());
			if(!dir.exists()) {
				dir.mkdirs();
			}
			String id = null;
			File file = null;
			FileOutputStream fout = null;
			IApiProfile profile = null;
			for(Iterator iter = profilecache.keySet().iterator(); iter.hasNext();) {
				id = (String) iter.next();
				profile = (IApiProfile) profilecache.get(id);
				file = savelocation.append(id+".profile").toFile(); //$NON-NLS-1$
				if(!file.exists()) {
					file.createNewFile();
				}
				try {
					fout = new FileOutputStream(file);
					profile.writeProfileDescription(fout);
					fout.flush();
				}
				finally {
					fout.close();
				}
			}
		}
	}	
	
	/**
	 * Throws a core exception with the given message and underlying exception,
	 * if any.
	 * 
	 * @param message error message
	 * @param e underlying exception or <code>null
	 * @throws CoreException
	 */
	private static void abort(String message, Throwable e) throws CoreException {
		throw new CoreException(new Status(IStatus.ERROR, ApiPlugin.PLUGIN_ID, message, e));
	}	
	
	/**
	 * Constructs and returns a profile from the given input stream (persisted profile).
	 * 
	 * @param stream input stream
	 * @return API profile
	 * @throws CoreException if unable to restore the profile
	 */
	public static IApiProfile restoreProfile(InputStream stream) throws CoreException {
		long start = System.currentTimeMillis();
		DocumentBuilder parser = null;
		try {
			parser = DocumentBuilderFactory.newInstance().newDocumentBuilder();
			parser.setErrorHandler(new DefaultHandler());
		} catch (ParserConfigurationException e) {
			abort("Error restoring API profile", e); //$NON-NLS-1$
		} catch (FactoryConfigurationError e) {
			abort("Error restoring API profile", e); //$NON-NLS-1$
		}
		IApiProfile profile = null;
		try {
			Document document = parser.parse(stream);
			Element root = document.getDocumentElement();
			if(root.getNodeName().equals(IApiXmlConstants.ELEMENT_APIPROFILE)) {
				profile = new ApiProfile(root.getAttribute(IApiXmlConstants.ATTR_NAME));
				// un-pooled components
				NodeList children = root.getElementsByTagName(IApiXmlConstants.ELEMENT_APICOMPONENT);
				List components = new ArrayList();
				for(int j = 0; j < children.getLength(); j++) {
					Element componentNode = (Element) children.item(j);
					// this also contains components in pools, so don't process them
					if (componentNode.getParentNode().equals(root)) {
						String location = componentNode.getAttribute(IApiXmlConstants.ATTR_LOCATION);
						IApiComponent component = profile.newApiComponent(Path.fromPortableString(location).toOSString());
						if(component != null) {
							components.add(component);
						}
					}
				}
				// pooled components
				children = root.getElementsByTagName(IApiXmlConstants.ELEMENT_POOL);
				for(int j = 0; j < children.getLength(); j++) {
					String location = ((Element) children.item(j)).getAttribute(IApiXmlConstants.ATTR_LOCATION);
					IPath poolPath = Path.fromPortableString(location);
					NodeList componentNodes = root.getElementsByTagName(IApiXmlConstants.ELEMENT_APICOMPONENT);
					for (int i = 0; i < componentNodes.getLength(); i++) {
						Element compElement = (Element) componentNodes.item(i);
						String id = compElement.getAttribute(IApiXmlConstants.ATTR_ID);
						String ver = compElement.getAttribute(IApiXmlConstants.ATTR_VERSION);
						StringBuffer name = new StringBuffer();
						name.append(id);
						name.append('_');
						name.append(ver);
						File file = poolPath.append(name.toString()).toFile();
						if (!file.exists()) {
							name.append(".jar"); //$NON-NLS-1$
							file = poolPath.append(name.toString()).toFile();
						}
						IApiComponent component = profile.newApiComponent(file.getAbsolutePath());
						if(component != null) {
							components.add(component);
						}
					}
					
				}
				profile.addApiComponents((IApiComponent[]) components.toArray(new IApiComponent[components.size()]));
			}
		} catch (IOException e) {
			abort("Error restoring API profile", e); //$NON-NLS-1$
		} catch(SAXException e) {
			abort("Error restoring API profile", e); //$NON-NLS-1$
		} finally {
			try {
				stream.close();
			} catch (IOException io) {
				ApiPlugin.log(io);
			}
		}
		if (profile == null) {
			abort("Invalid profile description", null); //$NON-NLS-1$
		}
		if(DEBUG) {
			System.out.println("Time to restore a persisted profile : " + (System.currentTimeMillis() - start) + "ms"); //$NON-NLS-1$ //$NON-NLS-2$
		}
		return profile;
	}
	
	/* (non-Javadoc)
	 * @see org.eclipse.core.resources.ISaveParticipant#saving(org.eclipse.core.resources.ISaveContext)
	 */
	public void saving(ISaveContext context) throws CoreException {
		if(!fNeedsSaving) {
			return;
		}
		try {
			persistStateCache();
			fNeedsSaving = false;
		} catch (IOException e) {
			ApiPlugin.log(e);
		}
	}
	
	/**
	 * Returns if the given name is an existing profile name
	 * @param name
	 * @return true if the given name is an existing profile name, false otherwise
	 */
	public boolean isExistingProfileName(String name) {
		if(profilecache == null) {
			return false;
		}
		return profilecache.keySet().contains(name);
	}
	
	/**
	 * Cleans up the manager and persists any unsaved API profiles
	 */
	public void stop() {
		try {
			// we should first dispose all existing profiles
			for (Iterator iterator = this.profilecache.values().iterator(); iterator.hasNext();) {
				IApiProfile profile = (IApiProfile) iterator.next();
				profile.dispose();
			}
			this.profilecache.clear();
			if(this.workspaceprofile != null) {
				this.workspaceprofile.dispose();
			}
		}
		finally {
			ApiPlugin.getDefault().removeSaveParticipant(this);
			JavaCore.removeElementChangedListener(this);
			PDECore.getDefault().getModelManager().removePluginModelListener(this);
			ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);
		}
	}

	/* (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.pde.api.tools.IApiProfileManager#getDefaultApiProfile()
	 */
	public synchronized IApiProfile getDefaultApiProfile() {
		initializeStateCache();
		return (IApiProfile) profilecache.get(defaultprofile);
	}

	/* (non-Javadoc)
	 * @see org.eclipse.pde.api.tools.IApiProfileManager#setDefaultApiProfile(java.lang.String)
	 */
	public void setDefaultApiProfile(String name) {
		fNeedsSaving = true;
		defaultprofile = name;
	}
	
	/* (non-Javadoc)
	 * @see org.eclipse.pde.api.tools.internal.provisional.IApiProfileManager#getWorkspaceProfile()
	 */
	public synchronized IApiProfile getWorkspaceProfile() {
		if(ApiPlugin.isRunningInFramework()) {
			if(this.workspaceprofile == null) {
				this.workspaceprofile = createWorkspaceProfile();
			}
			return this.workspaceprofile;
		}
		return null;
	}	
	
	/**
	 * Disposes the workspace profile such that a new one will be created
	 * on the next request.
	 */
	private synchronized void disposeWorkspaceProfile() {
		if (workspaceprofile != null) {
			workspaceprofile.dispose();
			workspaceprofile = null;
		}
	}
		
	/**
	 * Creates a workspace {@link IApiProfile}
	 * @return a new workspace {@link IApiProfile} or <code>null
	 */
	private IApiProfile createWorkspaceProfile() {
		long time = System.currentTimeMillis();
		IApiProfile profile = null; 
		try {
			profile = Factory.newApiProfile(ApiPlugin.WORKSPACE_API_PROFILE_ID);
			// populate it with only projects that are API aware
			IPluginModelBase[] models = PluginRegistry.getActiveModels();
			List componentsList = new ArrayList(models.length);
			IApiComponent apiComponent = null;
			for (int i = 0, length = models.length; i < length; i++) {
				try {
					apiComponent = profile.newApiComponent(models[i]);
					if (apiComponent != null) {
						componentsList.add(apiComponent);
					}
				} catch (CoreException e) {
					ApiPlugin.log(e);
				}
			}
			profile.addApiComponents((IApiComponent[]) componentsList.toArray(new IApiComponent[componentsList.size()]));
		} finally {
			if (DEBUG) {
				System.out.println("Time to create a workspace profile : " + (System.currentTimeMillis() - time) + "ms"); //$NON-NLS-1$ //$NON-NLS-2$
			}
		}
		return profile;
	}
	
	/* (non-Javadoc)
	 * @see org.eclipse.jdt.core.IElementChangedListener#elementChanged(org.eclipse.jdt.core.ElementChangedEvent)
	 */
	public void elementChanged(ElementChangedEvent event) {
		Object obj = event.getSource();
		if(obj instanceof IJavaElementDelta) {
			processJavaElementDeltas(((IJavaElementDelta)obj).getAffectedChildren(), null);
		}
	}
	
	/**
	 * Processes the java element deltas of interest
	 * @param deltas
	 */
	private synchronized void processJavaElementDeltas(IJavaElementDelta[] deltas, IJavaProject project) {
		try {
			IJavaElementDelta delta = null;
			for(int i = 0; i < deltas.length; i++) {
				delta = deltas[i];
				switch(delta.getElement().getElementType()) {
					case IJavaElement.JAVA_PROJECT: {
						IJavaProject proj = (IJavaProject) delta.getElement();
						IProject pj = proj.getProject();
						if (acceptProject(pj)) {
							switch (delta.getKind()) {
								//process the project changed only if the project is API aware
							case IJavaElementDelta.CHANGED:
								int flags = delta.getFlags();
								if( (flags & IJavaElementDelta.F_RESOLVED_CLASSPATH_CHANGED) != 0 ||
									(flags & IJavaElementDelta.F_CLASSPATH_CHANGED) != 0 ||
									(flags & IJavaElementDelta.F_CLOSED) != 0 ||
									(flags & IJavaElementDelta.F_OPENED) != 0) {
										if(DEBUG) {
											System.out.println("--> processing CLASSPATH CHANGE/CLOSE/OPEN project: ["+proj.getElementName()+"]"); //$NON-NLS-1$ //$NON-NLS-2$
										}
										disposeWorkspaceProfile();
								} else if((flags & IJavaElementDelta.F_CHILDREN) != 0) {
									if(DEBUG) {
										System.out.println("--> processing child deltas of project: ["+proj.getElementName()+"]"); //$NON-NLS-1$ //$NON-NLS-2$
									}
									processJavaElementDeltas(delta.getAffectedChildren(), proj);
								}
								break;
							}
						}
						break;
					}
					case IJavaElement.PACKAGE_FRAGMENT_ROOT: {
						IPackageFragmentRoot root = (IPackageFragmentRoot) delta.getElement();
						if(DEBUG) {
							System.out.println("processed package fragment root delta: ["+root.getElementName()+"]"); //$NON-NLS-1$ //$NON-NLS-2$
						}
						switch(delta.getKind()) {
							case IJavaElementDelta.CHANGED: {
								if(DEBUG) {
									System.out.println("processed children of CHANGED package fragment root: ["+root.getElementName()+"]"); //$NON-NLS-1$ //$NON-NLS-2$
								}
								processJavaElementDeltas(delta.getAffectedChildren(), project);
								break;
							}
						}
						break;
					}
					case IJavaElement.PACKAGE_FRAGMENT: {
						IPackageFragment fragment = (IPackageFragment) delta.getElement();
						if(delta.getKind() == IJavaElementDelta.REMOVED) {
							handlePackageRemoval(project.getProject(), fragment);
						}
						break;
					}
				}
			}
		} catch (CoreException e) {
			ApiPlugin.log(e);
		}
	}
		
	/**
	 * Handles the specified {@link IPackageFragment} being removed.
	 * When a packaged is removed, we:
	 * <ol>
	 * <li>Remove the package from the cache of resolved providers
	 * 	of that package (in the API profile)</li>
	 * </ol>
	 * @param project
	 * @param fragment
	 * @throws CoreException
	 */
	private void handlePackageRemoval(IProject project, IPackageFragment fragment) throws CoreException {
		if(DEBUG) {
			System.out.println("processed package fragment REMOVE delta: ["+fragment.getElementName()+"]"); //$NON-NLS-1$ //$NON-NLS-2$
		}
		((ApiProfile)getWorkspaceProfile()).clearPackage(fragment.getElementName());
	}
	
	/**
	 * Returns if we should care about the specified project
	 * @param project
	 * @return true if the project is an 'API aware' project, false otherwise
	 */
	private boolean acceptProject(IProject project) {
		try {
			if (!project.isOpen()) {
				return true;
			}
			return project.exists() && project.hasNature(ApiPlugin.NATURE_ID);
		}
		catch(CoreException e) {
			return false;
		}
	}
	
	/* (non-Javadoc)
	 * 
	 * Whenever a bundle definition changes (add/removed/changed), the 
	 * workspace profile becomes potentially invalid as the bundle description
	 * may have changed in some way to invalidate our underlying OSGi state.
	 * 
	 * @see org.eclipse.pde.internal.core.IPluginModelListener#modelsChanged(org.eclipse.pde.internal.core.PluginModelDelta)
	 */
	public void modelsChanged(PluginModelDelta delta) {
		ModelEntry[] entries = null;
		switch(delta.getKind()) {
			case PluginModelDelta.ADDED: {
				entries = delta.getAddedEntries();
				break;
			}
			case PluginModelDelta.REMOVED: {
				entries = delta.getRemovedEntries();
				break;
			}
			case PluginModelDelta.CHANGED: {
				entries = delta.getChangedEntries();
				break;
			}
		}
		if(entries != null) {
			IPluginModelBase model = null;
			for(int i = 0; i < entries.length; i++) {
				model = entries[i].getModel();
				if(model != null) {
					disposeWorkspaceProfile();
				}
			}
		}
	}

	/* (non-Javadoc)
	 * @see org.eclipse.core.resources.IResourceChangeListener#resourceChanged(org.eclipse.core.resources.IResourceChangeEvent)
	 */
	public void resourceChanged(IResourceChangeEvent event) {
		// clean all API errors when a project description changes
		IResourceDelta delta = event.getDelta();
		if (delta != null) {
			IResourceDelta[] children = delta.getAffectedChildren(IResourceDelta.CHANGED);
			for (int i = 0; i < children.length; i++) {
				IResourceDelta d = children[i];
				IResource resource = d.getResource();
				if (resource.getType() == IResource.PROJECT) {
					if ((d.getFlags() & IResourceDelta.DESCRIPTION) != 0) {
						IProject project = (IProject)resource;
						if (project.isAccessible()) {
							try {
								if (!project.getDescription().hasNature(ApiPlugin.NATURE_ID)) {
									IJavaProject jp = JavaCore.create(project);
									if (jp.exists()) {
										ApiDescriptionManager.getDefault().clean(jp, true, true);
										ApiAnalysisBuilder.cleanupMarkers(resource);
									}
								}
							} catch (CoreException e) {
								ApiPlugin.log(e.getStatus());
							}
						}
					}
				}
			}
		}
	}

}
... this post is sponsored by my books ...

#1 New Release!

FP Best Seller

 

new blog posts

 

Copyright 1998-2021 Alvin Alexander, alvinalexander.com
All Rights Reserved.

A percentage of advertising revenue from
pages under the /java/jwarehouse URI on this website is
paid back to open source projects.