|
Java example source code file (DnsContext.java)
The DnsContext.java Java example source code/* * Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package com.sun.jndi.dns; import java.util.Enumeration; import java.util.Hashtable; import javax.naming.*; import javax.naming.directory.*; import javax.naming.spi.DirectoryManager; import com.sun.jndi.toolkit.ctx.*; /** * A DnsContext is a directory context representing a DNS node. * * @author Scott Seligman */ public class DnsContext extends ComponentDirContext { DnsName domain; // fully-qualified domain name of this context, // with a root (empty) label at position 0 Hashtable<Object,Object> environment; private boolean envShared; // true if environment is possibly shared // and so must be copied on write private boolean parentIsDns; // was this DnsContext created by // another? see composeName() private String[] servers; private Resolver resolver; private boolean authoritative; // must all responses be authoritative? private boolean recursion; // request recursion on queries? private int timeout; // initial timeout on UDP queries in ms private int retries; // number of UDP retries static final NameParser nameParser = new DnsNameParser(); // Timeouts for UDP queries use exponential backoff: each retry // is for twice as long as the last. The following constants set // the defaults for the initial timeout (in ms) and the number of // retries, and name the environment properties used to override // these defaults. private static final int DEFAULT_INIT_TIMEOUT = 1000; private static final int DEFAULT_RETRIES = 4; private static final String INIT_TIMEOUT = "com.sun.jndi.dns.timeout.initial"; private static final String RETRIES = "com.sun.jndi.dns.timeout.retries"; // The resource record type and class to use for lookups, and the // property used to modify them private CT lookupCT; private static final String LOOKUP_ATTR = "com.sun.jndi.dns.lookup.attr"; // Property used to disallow recursion on queries private static final String RECURSION = "com.sun.jndi.dns.recursion"; // ANY == ResourceRecord.QCLASS_STAR == ResourceRecord.QTYPE_STAR private static final int ANY = ResourceRecord.QTYPE_STAR; // The zone tree used for list operations private static final ZoneNode zoneTree = new ZoneNode(null); /** * Returns a DNS context for a given domain and servers. * Each server is of the form "server[:port]". * IPv6 literal host names include delimiting brackets. * There must be at least one server. * The environment must not be null; it is cloned before being stored. */ @SuppressWarnings("unchecked") public DnsContext(String domain, String[] servers, Hashtable<?,?> environment) throws NamingException { this.domain = new DnsName(domain.endsWith(".") ? domain : domain + "."); this.servers = (servers == null) ? null : servers.clone(); this.environment = (Hashtable<Object,Object>) environment.clone(); envShared = false; parentIsDns = false; resolver = null; initFromEnvironment(); } /* * Returns a clone of a DNS context, just like DnsContext(DnsContext) * but with a different domain name and with parentIsDns set to true. */ DnsContext(DnsContext ctx, DnsName domain) { this(ctx); this.domain = domain; parentIsDns = true; } /* * Returns a clone of a DNS context. The context's modifiable * private state is independent of the original's (so closing one * context, for example, won't close the other). The two contexts * share <tt>environment, but it's copy-on-write so there's * no conflict. */ private DnsContext(DnsContext ctx) { environment = ctx.environment; // shared environment, copy-on-write envShared = ctx.envShared = true; parentIsDns = ctx.parentIsDns; domain = ctx.domain; servers = ctx.servers; // shared servers, no write operation resolver = ctx.resolver; authoritative = ctx.authoritative; recursion = ctx.recursion; timeout = ctx.timeout; retries = ctx.retries; lookupCT = ctx.lookupCT; } public void close() { if (resolver != null) { resolver.close(); resolver = null; } } //---------- Environment operations /* * Override default with a noncloning version. */ protected Hashtable<?,?> p_getEnvironment() { return environment; } public Hashtable<?,?> getEnvironment() throws NamingException { return (Hashtable<?,?>) environment.clone(); } @SuppressWarnings("unchecked") public Object addToEnvironment(String propName, Object propVal) throws NamingException { if (propName.equals(LOOKUP_ATTR)) { lookupCT = getLookupCT((String) propVal); } else if (propName.equals(Context.AUTHORITATIVE)) { authoritative = "true".equalsIgnoreCase((String) propVal); } else if (propName.equals(RECURSION)) { recursion = "true".equalsIgnoreCase((String) propVal); } else if (propName.equals(INIT_TIMEOUT)) { int val = Integer.parseInt((String) propVal); if (timeout != val) { timeout = val; resolver = null; } } else if (propName.equals(RETRIES)) { int val = Integer.parseInt((String) propVal); if (retries != val) { retries = val; resolver = null; } } if (!envShared) { return environment.put(propName, propVal); } else if (environment.get(propName) != propVal) { // copy on write environment = (Hashtable<Object,Object>) environment.clone(); envShared = false; return environment.put(propName, propVal); } else { return propVal; } } @SuppressWarnings("unchecked") public Object removeFromEnvironment(String propName) throws NamingException { if (propName.equals(LOOKUP_ATTR)) { lookupCT = getLookupCT(null); } else if (propName.equals(Context.AUTHORITATIVE)) { authoritative = false; } else if (propName.equals(RECURSION)) { recursion = true; } else if (propName.equals(INIT_TIMEOUT)) { if (timeout != DEFAULT_INIT_TIMEOUT) { timeout = DEFAULT_INIT_TIMEOUT; resolver = null; } } else if (propName.equals(RETRIES)) { if (retries != DEFAULT_RETRIES) { retries = DEFAULT_RETRIES; resolver = null; } } if (!envShared) { return environment.remove(propName); } else if (environment.get(propName) != null) { // copy-on-write environment = (Hashtable<Object,Object>) environment.clone(); envShared = false; return environment.remove(propName); } else { return null; } } /* * Update PROVIDER_URL property. Call this only when environment * is not being shared. */ void setProviderUrl(String url) { // assert !envShared; environment.put(Context.PROVIDER_URL, url); } /* * Read environment properties and set parameters. */ private void initFromEnvironment() throws InvalidAttributeIdentifierException { lookupCT = getLookupCT((String) environment.get(LOOKUP_ATTR)); authoritative = "true".equalsIgnoreCase((String) environment.get(Context.AUTHORITATIVE)); String val = (String) environment.get(RECURSION); recursion = ((val == null) || "true".equalsIgnoreCase(val)); val = (String) environment.get(INIT_TIMEOUT); timeout = (val == null) ? DEFAULT_INIT_TIMEOUT : Integer.parseInt(val); val = (String) environment.get(RETRIES); retries = (val == null) ? DEFAULT_RETRIES : Integer.parseInt(val); } private CT getLookupCT(String attrId) throws InvalidAttributeIdentifierException { return (attrId == null) ? new CT(ResourceRecord.CLASS_INTERNET, ResourceRecord.TYPE_TXT) : fromAttrId(attrId); } //---------- Naming operations public Object c_lookup(Name name, Continuation cont) throws NamingException { cont.setSuccess(); if (name.isEmpty()) { DnsContext ctx = new DnsContext(this); ctx.resolver = new Resolver(servers, timeout, retries); // clone for parallelism return ctx; } try { DnsName fqdn = fullyQualify(name); ResourceRecords rrs = getResolver().query(fqdn, lookupCT.rrclass, lookupCT.rrtype, recursion, authoritative); Attributes attrs = rrsToAttrs(rrs, null); DnsContext ctx = new DnsContext(this, fqdn); return DirectoryManager.getObjectInstance(ctx, name, this, environment, attrs); } catch (NamingException e) { cont.setError(this, name); throw cont.fillInException(e); } catch (Exception e) { cont.setError(this, name); NamingException ne = new NamingException( "Problem generating object using object factory"); ne.setRootCause(e); throw cont.fillInException(ne); } } public Object c_lookupLink(Name name, Continuation cont) throws NamingException { return c_lookup(name, cont); } public NamingEnumeration<NameClassPair> c_list(Name name, Continuation cont) throws NamingException { cont.setSuccess(); try { DnsName fqdn = fullyQualify(name); NameNode nnode = getNameNode(fqdn); DnsContext ctx = new DnsContext(this, fqdn); return new NameClassPairEnumeration(ctx, nnode.getChildren()); } catch (NamingException e) { cont.setError(this, name); throw cont.fillInException(e); } } public NamingEnumeration<Binding> c_listBindings(Name name, Continuation cont) throws NamingException { cont.setSuccess(); try { DnsName fqdn = fullyQualify(name); NameNode nnode = getNameNode(fqdn); DnsContext ctx = new DnsContext(this, fqdn); return new BindingEnumeration(ctx, nnode.getChildren()); } catch (NamingException e) { cont.setError(this, name); throw cont.fillInException(e); } } public void c_bind(Name name, Object obj, Continuation cont) throws NamingException { cont.setError(this, name); throw cont.fillInException( new OperationNotSupportedException()); } public void c_rebind(Name name, Object obj, Continuation cont) throws NamingException { cont.setError(this, name); throw cont.fillInException( new OperationNotSupportedException()); } public void c_unbind(Name name, Continuation cont) throws NamingException { cont.setError(this, name); throw cont.fillInException( new OperationNotSupportedException()); } public void c_rename(Name oldname, Name newname, Continuation cont) throws NamingException { cont.setError(this, oldname); throw cont.fillInException( new OperationNotSupportedException()); } public Context c_createSubcontext(Name name, Continuation cont) throws NamingException { cont.setError(this, name); throw cont.fillInException( new OperationNotSupportedException()); } public void c_destroySubcontext(Name name, Continuation cont) throws NamingException { cont.setError(this, name); throw cont.fillInException( new OperationNotSupportedException()); } public NameParser c_getNameParser(Name name, Continuation cont) throws NamingException { cont.setSuccess(); return nameParser; } //---------- Directory operations public void c_bind(Name name, Object obj, Attributes attrs, Continuation cont) throws NamingException { cont.setError(this, name); throw cont.fillInException( new OperationNotSupportedException()); } public void c_rebind(Name name, Object obj, Attributes attrs, Continuation cont) throws NamingException { cont.setError(this, name); throw cont.fillInException( new OperationNotSupportedException()); } public DirContext c_createSubcontext(Name name, Attributes attrs, Continuation cont) throws NamingException { cont.setError(this, name); throw cont.fillInException( new OperationNotSupportedException()); } public Attributes c_getAttributes(Name name, String[] attrIds, Continuation cont) throws NamingException { cont.setSuccess(); try { DnsName fqdn = fullyQualify(name); CT[] cts = attrIdsToClassesAndTypes(attrIds); CT ct = getClassAndTypeToQuery(cts); ResourceRecords rrs = getResolver().query(fqdn, ct.rrclass, ct.rrtype, recursion, authoritative); return rrsToAttrs(rrs, cts); } catch (NamingException e) { cont.setError(this, name); throw cont.fillInException(e); } } public void c_modifyAttributes(Name name, int mod_op, Attributes attrs, Continuation cont) throws NamingException { cont.setError(this, name); throw cont.fillInException( new OperationNotSupportedException()); } public void c_modifyAttributes(Name name, ModificationItem[] mods, Continuation cont) throws NamingException { cont.setError(this, name); throw cont.fillInException( new OperationNotSupportedException()); } public NamingEnumeration<SearchResult> c_search(Name name, Attributes matchingAttributes, String[] attributesToReturn, Continuation cont) throws NamingException { throw new OperationNotSupportedException(); } public NamingEnumeration<SearchResult> c_search(Name name, String filter, SearchControls cons, Continuation cont) throws NamingException { throw new OperationNotSupportedException(); } public NamingEnumeration<SearchResult> c_search(Name name, String filterExpr, Object[] filterArgs, SearchControls cons, Continuation cont) throws NamingException { throw new OperationNotSupportedException(); } public DirContext c_getSchema(Name name, Continuation cont) throws NamingException { cont.setError(this, name); throw cont.fillInException( new OperationNotSupportedException()); } public DirContext c_getSchemaClassDefinition(Name name, Continuation cont) throws NamingException { cont.setError(this, name); throw cont.fillInException( new OperationNotSupportedException()); } //---------- Name-related operations public String getNameInNamespace() { return domain.toString(); } public Name composeName(Name name, Name prefix) throws NamingException { Name result; // Any name that's not a CompositeName is assumed to be a DNS // compound name. Convert each to a DnsName for syntax checking. if (!(prefix instanceof DnsName || prefix instanceof CompositeName)) { prefix = (new DnsName()).addAll(prefix); } if (!(name instanceof DnsName || name instanceof CompositeName)) { name = (new DnsName()).addAll(name); } // Each of prefix and name is now either a DnsName or a CompositeName. // If we have two DnsNames, simply join them together. if ((prefix instanceof DnsName) && (name instanceof DnsName)) { result = (DnsName) (prefix.clone()); result.addAll(name); return new CompositeName().add(result.toString()); } // Wrap compound names in composite names. Name prefixC = (prefix instanceof CompositeName) ? prefix : new CompositeName().add(prefix.toString()); Name nameC = (name instanceof CompositeName) ? name : new CompositeName().add(name.toString()); int prefixLast = prefixC.size() - 1; // Let toolkit do the work at namespace boundaries. if (nameC.isEmpty() || nameC.get(0).equals("") || prefixC.isEmpty() || prefixC.get(prefixLast).equals("")) { return super.composeName(nameC, prefixC); } result = (prefix == prefixC) ? (CompositeName) prefixC.clone() : prefixC; // prefixC is already a clone result.addAll(nameC); if (parentIsDns) { DnsName dnsComp = (prefix instanceof DnsName) ? (DnsName) prefix.clone() : new DnsName(prefixC.get(prefixLast)); dnsComp.addAll((name instanceof DnsName) ? name : new DnsName(nameC.get(0))); result.remove(prefixLast + 1); result.remove(prefixLast); result.add(prefixLast, dnsComp.toString()); } return result; } //---------- Helper methods /* * Resolver is not created until needed, to allow time for updates * to the environment. */ private synchronized Resolver getResolver() throws NamingException { if (resolver == null) { resolver = new Resolver(servers, timeout, retries); } return resolver; } /* * Returns the fully-qualified domain name of a name given * relative to this context. Result includes a root label (an * empty component at position 0). */ DnsName fullyQualify(Name name) throws NamingException { if (name.isEmpty()) { return domain; } DnsName dnsName = (name instanceof CompositeName) ? new DnsName(name.get(0)) // parse name : (DnsName) (new DnsName()).addAll(name); // clone & check syntax if (dnsName.hasRootLabel()) { // Be overly generous and allow root label if we're in root domain. if (domain.size() == 1) { return dnsName; } else { throw new InvalidNameException( "DNS name " + dnsName + " not relative to " + domain); } } return (DnsName) dnsName.addAll(0, domain); } /* * Converts resource records to an attribute set. Only resource * records in the answer section are used, and only those that * match the classes and types in cts (see classAndTypeMatch() * for matching rules). */ private static Attributes rrsToAttrs(ResourceRecords rrs, CT[] cts) { BasicAttributes attrs = new BasicAttributes(true); for (int i = 0; i < rrs.answer.size(); i++) { ResourceRecord rr = rrs.answer.elementAt(i); int rrtype = rr.getType(); int rrclass = rr.getRrclass(); if (!classAndTypeMatch(rrclass, rrtype, cts)) { continue; } String attrId = toAttrId(rrclass, rrtype); Attribute attr = attrs.get(attrId); if (attr == null) { attr = new BasicAttribute(attrId); attrs.put(attr); } attr.add(rr.getRdata()); } return attrs; } /* * Returns true if rrclass and rrtype match some element of cts. * A match occurs if corresponding classes and types are equal, * or if the array value is ANY. If cts is null, then any class * and type match. */ private static boolean classAndTypeMatch(int rrclass, int rrtype, CT[] cts) { if (cts == null) { return true; } for (int i = 0; i < cts.length; i++) { CT ct = cts[i]; boolean classMatch = (ct.rrclass == ANY) || (ct.rrclass == rrclass); boolean typeMatch = (ct.rrtype == ANY) || (ct.rrtype == rrtype); if (classMatch && typeMatch) { return true; } } return false; } /* * Returns the attribute ID for a resource record given its class * and type. If the record is in the internet class, the * corresponding attribute ID is the record's type name (or the * integer type value if the name is not known). If the record is * not in the internet class, the class name (or integer class * value) is prepended to the attribute ID, separated by a space. * * A class or type value of ANY represents an indeterminate class * or type, and is represented within the attribute ID by "*". * For example, the attribute ID "IN *" represents * any type in the internet class, and "* NS" represents an NS * record of any class. */ private static String toAttrId(int rrclass, int rrtype) { String attrId = ResourceRecord.getTypeName(rrtype); if (rrclass != ResourceRecord.CLASS_INTERNET) { attrId = ResourceRecord.getRrclassName(rrclass) + " " + attrId; } return attrId; } /* * Returns the class and type values corresponding to an attribute * ID. An indeterminate class or type is represented by ANY. See * toAttrId() for the format of attribute IDs. * * @throws InvalidAttributeIdentifierException * if class or type is unknown */ private static CT fromAttrId(String attrId) throws InvalidAttributeIdentifierException { if (attrId.equals("")) { throw new InvalidAttributeIdentifierException( "Attribute ID cannot be empty"); } int rrclass; int rrtype; int space = attrId.indexOf(' '); // class if (space < 0) { rrclass = ResourceRecord.CLASS_INTERNET; } else { String className = attrId.substring(0, space); rrclass = ResourceRecord.getRrclass(className); if (rrclass < 0) { throw new InvalidAttributeIdentifierException( "Unknown resource record class '" + className + '\''); } } // type String typeName = attrId.substring(space + 1); rrtype = ResourceRecord.getType(typeName); if (rrtype < 0) { throw new InvalidAttributeIdentifierException( "Unknown resource record type '" + typeName + '\''); } return new CT(rrclass, rrtype); } /* * Returns an array of the classes and types corresponding to a * set of attribute IDs. See toAttrId() for the format of * attribute IDs, and classAndTypeMatch() for the format of the * array returned. */ private static CT[] attrIdsToClassesAndTypes(String[] attrIds) throws InvalidAttributeIdentifierException { if (attrIds == null) { return null; } CT[] cts = new CT[attrIds.length]; for (int i = 0; i < attrIds.length; i++) { cts[i] = fromAttrId(attrIds[i]); } return cts; } /* * Returns the most restrictive resource record class and type * that may be used to query for records matching cts. * See classAndTypeMatch() for matching rules. */ private static CT getClassAndTypeToQuery(CT[] cts) { int rrclass; int rrtype; if (cts == null) { // Query all records. rrclass = ANY; rrtype = ANY; } else if (cts.length == 0) { // No records are requested, but we need to ask for something. rrclass = ResourceRecord.CLASS_INTERNET; rrtype = ANY; } else { rrclass = cts[0].rrclass; rrtype = cts[0].rrtype; for (int i = 1; i < cts.length; i++) { if (rrclass != cts[i].rrclass) { rrclass = ANY; } if (rrtype != cts[i].rrtype) { rrtype = ANY; } } } return new CT(rrclass, rrtype); } //---------- Support for list operations /* * Synchronization notes: * * Any access to zoneTree that walks the tree, whether it modifies * the tree or not, is synchronized on zoneTree. * [%%% Note: a read/write lock would allow increased concurrency.] * The depth of a ZoneNode can thereafter be accessed without * further synchronization. Access to other fields and methods * should be synchronized on the node itself. * * A zone's contents is a NameNode tree that, once created, is never * modified. The only synchronization needed is to ensure that it * gets flushed into shared memory after being created, which is * accomplished by ZoneNode.populate(). The contents are accessed * via a soft reference, so a ZoneNode may be seen to be populated * one moment and unpopulated the next. */ /* * Returns the node in the zone tree corresponding to a * fully-qualified domain name. If the desired portion of the * tree has not yet been populated or has been outdated, a zone * transfer is done to populate the tree. */ private NameNode getNameNode(DnsName fqdn) throws NamingException { dprint("getNameNode(" + fqdn + ")"); // Find deepest related zone in zone tree. ZoneNode znode; DnsName zone; synchronized (zoneTree) { znode = zoneTree.getDeepestPopulated(fqdn); } dprint("Deepest related zone in zone tree: " + ((znode != null) ? znode.getLabel() : "[none]")); NameNode topOfZone; NameNode nnode; if (znode != null) { synchronized (znode) { topOfZone = znode.getContents(); } // If fqdn is in znode's zone, is not at a zone cut, and // is current, we're done. if (topOfZone != null) { nnode = topOfZone.get(fqdn, znode.depth() + 1); // +1 for root if ((nnode != null) && !nnode.isZoneCut()) { dprint("Found node " + fqdn + " in zone tree"); zone = (DnsName) fqdn.getPrefix(znode.depth() + 1); // +1 for root boolean current = isZoneCurrent(znode, zone); boolean restart = false; synchronized (znode) { if (topOfZone != znode.getContents()) { // Zone was modified while we were examining it. // All bets are off. restart = true; } else if (!current) { znode.depopulate(); } else { return nnode; // cache hit! } } dprint("Zone not current; discarding node"); if (restart) { return getNameNode(fqdn); } } } } // Cache miss... do it the expensive way. dprint("Adding node " + fqdn + " to zone tree"); // Find fqdn's zone and add it to the tree. zone = getResolver().findZoneName(fqdn, ResourceRecord.CLASS_INTERNET, recursion); dprint("Node's zone is " + zone); synchronized (zoneTree) { znode = (ZoneNode) zoneTree.add(zone, 1); // "1" to skip root } // If znode is now populated we know -- because the first half of // getNodeName() didn't find it -- that it was populated by another // thread during this method call. Assume then that it's current. synchronized (znode) { topOfZone = znode.isPopulated() ? znode.getContents() : populateZone(znode, zone); } // Desired node should now be in znode's populated zone. Find it. nnode = topOfZone.get(fqdn, zone.size()); if (nnode == null) { throw new ConfigurationException( "DNS error: node not found in its own zone"); } dprint("Found node in newly-populated zone"); return nnode; } /* * Does a zone transfer to [re]populate a zone in the zone tree. * Returns the zone's new contents. */ private NameNode populateZone(ZoneNode znode, DnsName zone) throws NamingException { dprint("Populating zone " + zone); // assert Thread.holdsLock(znode); ResourceRecords rrs = getResolver().queryZone(zone, ResourceRecord.CLASS_INTERNET, recursion); dprint("zone xfer complete: " + rrs.answer.size() + " records"); return znode.populate(zone, rrs); } /* * Determine if a ZoneNode's data is current. * We base this on a comparison between the cached serial * number and the latest SOA record. * * If there is no SOA record, znode is not (or is no longer) a zone: * depopulate znode and return false. * * Since this method may perform a network operation, it is best * to call it with znode unlocked. Caller must then note that the * result may be outdated by the time this method returns. */ private boolean isZoneCurrent(ZoneNode znode, DnsName zone) throws NamingException { // former version: return !znode.isExpired(); if (!znode.isPopulated()) { return false; } ResourceRecord soa = getResolver().findSoa(zone, ResourceRecord.CLASS_INTERNET, recursion); synchronized (znode) { if (soa == null) { znode.depopulate(); } return (znode.isPopulated() && znode.compareSerialNumberTo(soa) >= 0); } } //---------- Debugging private static final boolean debug = false; private static final void dprint(String msg) { if (debug) { System.err.println("** " + msg); } } } //---------- /* * A pairing of a resource record class and a resource record type. * A value of ANY in either field represents an indeterminate value. */ class CT { int rrclass; int rrtype; CT(int rrclass, int rrtype) { this.rrclass = rrclass; this.rrtype = rrtype; } } //---------- /* * Common base class for NameClassPairEnumeration and BindingEnumeration. */ abstract class BaseNameClassPairEnumeration<T> implements NamingEnumeration Other Java examples (source code examples)Here is a short list of links related to this Java DnsContext.java source code file: |
... this post is sponsored by my books ... | |
#1 New Release! |
FP Best Seller |
Copyright 1998-2024 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.