/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.jini.jeri.kerberos; import com.sun.jini.logging.Levels; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.InputStream; import java.io.OutputStream; import java.io.IOException; import java.lang.ref.SoftReference; import java.lang.ref.ReferenceQueue; import java.net.Socket; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.logging.Level; import java.util.logging.Logger; import java.util.logging.LogRecord; import java.util.HashSet; import java.util.Map; import java.util.Set; import javax.security.auth.AuthPermission; import javax.security.auth.kerberos.KerberosPrincipal; import javax.security.auth.Subject; import net.jini.core.constraint.ClientAuthentication; import net.jini.core.constraint.ClientMaxPrincipal; import net.jini.core.constraint.ClientMaxPrincipalType; import net.jini.core.constraint.ClientMinPrincipal; import net.jini.core.constraint.ClientMinPrincipalType; import net.jini.core.constraint.Confidentiality; import net.jini.core.constraint.ConnectionAbsoluteTime; import net.jini.core.constraint.ConnectionRelativeTime; import net.jini.core.constraint.ConstraintAlternatives; import net.jini.core.constraint.Delegation; import net.jini.core.constraint.Integrity; import net.jini.core.constraint.InvocationConstraint; import net.jini.core.constraint.InvocationConstraints; import net.jini.core.constraint.ServerAuthentication; import net.jini.core.constraint.ServerMinPrincipal; import net.jini.io.UnsupportedConstraintException; import net.jini.security.AuthenticationPermission; import org.ietf.jgss.GSSContext; import org.ietf.jgss.GSSCredential; import org.ietf.jgss.GSSException; import org.ietf.jgss.GSSName; import org.ietf.jgss.GSSManager; import org.ietf.jgss.MessageProp; import org.ietf.jgss.Oid; /** * Utility class for the Kerberos provider. * * @author Sun Microsystems, Inc. * @since 2.0 */ class KerberosUtil { /** * Oid used to represent the Kerberos v5 GSS-API mechanism, * defined as in RFC 1964. */ static final Oid krb5MechOid; /** * Oid used to represent the name syntax in Kerberos v5 GSS-API * mechanism. Examples: "joe@KERBEROSREALM" or * "ftp/myhost.foo.com@KERBEROSREALM". */ static final Oid krb5NameType; static { try { krb5MechOid = new Oid("1.2.840.113554.1.2.2"); krb5NameType = new Oid("1.2.840.113554.1.2.2.1"); } catch (GSSException e) { throw new ExceptionInInitializerError(e); } } static final InvocationConstraints INTEGRITY_REQUIRED_CONSTRAINTS = new InvocationConstraints(Integrity.YES, null); static final InvocationConstraints INTEGRITY_PREFERRED_CONSTRAINTS = new InvocationConstraints(null, Integrity.YES); /** Field used by ConfigIter to generate configs */ private static final boolean BOOL_TABLE[] = new boolean[] {false, true}; /** Map constraints to other constraints they depend on */ private static final Map depends = new HashMap(); static { InvocationConstraint[] deps = new InvocationConstraint[0]; depends.put(ConnectionAbsoluteTime.class, deps); depends.put(ConnectionRelativeTime.class, deps); depends.put(Integrity.class, deps); depends.put(Confidentiality.class, deps); depends.put(ClientAuthentication.class, deps); depends.put(ServerAuthentication.class, deps); deps = new InvocationConstraint[]{ClientAuthentication.YES}; depends.put(ClientMinPrincipal.class, deps); depends.put(ClientMinPrincipalType.class, deps); depends.put(ClientMaxPrincipal.class, deps); depends.put(ClientMaxPrincipalType.class, deps); depends.put(Delegation.class, deps); deps = new InvocationConstraint[]{ServerAuthentication.YES}; depends.put(ServerMinPrincipal.class, deps); } /** * make the null constructor private, so this class is * non-instantiable */ private KerberosUtil() {} //----------------------------------- // package-private methods //----------------------------------- /** * Test whether the caller has AuthPermission("getSubject"). * * @return true if the caller has AuthPermission("getSubject"), * false otherwise. */ static boolean canGetSubject() { try { SecurityManager sm = System.getSecurityManager(); if (sm != null) sm.checkPermission(new AuthPermission("getSubject")); return true; } catch (SecurityException e) { return false; } } /** * Check whether the type of the specified constraint is supported * by this provider. * * @param c the constraint to be tested * @return true if the specified constraints has a known type, * false otherwise. */ static boolean isSupportedConstraintType(InvocationConstraint c) { return depends.get(c.getClass()) != null; } /** * Test whether the specified constraint can possibly be supported * by this provider. * * @param c the constraint to be tested * @return true if the specified constraints can possibly be * supported, false otherwise. */ static boolean isSupportableConstraint(InvocationConstraint c) { if (c instanceof ConstraintAlternatives) { Set alts = ((ConstraintAlternatives) c).elements(); Class type = null; for (Iterator iter = alts.iterator(); iter.hasNext(); ) { InvocationConstraint alt= (InvocationConstraint) iter.next(); if (type == null) { type = alt.getClass(); } else if (type != alt.getClass()) { return false; // does not support heterogenous alternatives } if (isSupportableConstraint(alt)) return true; } return false; } if (!isSupportedConstraintType(c)) return false; // unsupported constraint type if (c instanceof Integrity) { return c == Integrity.YES; } else if (c instanceof ClientAuthentication) { return c == ClientAuthentication.YES; } else if (c instanceof ServerAuthentication) { return c == ServerAuthentication.YES; } else if (c instanceof ClientMinPrincipal) { Set elems = ((ClientMinPrincipal) c).elements(); if (elems.size() > 1) { return false; // can only authenticate as one principal } else { // elems contains at least one element return elems.iterator().next() instanceof KerberosPrincipal; } } else if (c instanceof ClientMinPrincipalType) { Set elems = ((ClientMinPrincipalType) c).elements(); if (elems.size() > 1) { return false; // can only support one type } else { // elems contains at least one element return elems.contains(KerberosPrincipal.class); } } else if (c instanceof ClientMaxPrincipal) { Set elems = ((ClientMaxPrincipal) c).elements(); for (Iterator iter = elems.iterator(); iter.hasNext(); ) { if (iter.next() instanceof KerberosPrincipal) return true; } return false; } else if (c instanceof ClientMaxPrincipalType) { Set elems = ((ClientMaxPrincipalType) c).elements(); return elems.contains(KerberosPrincipal.class); } else if (c instanceof ServerMinPrincipal) { Set elems = ((ServerMinPrincipal) c).elements(); if (elems.size() > 1) { return false; // can only authenticate as one principal } else { // elems contains at least one element return elems.iterator().next() instanceof KerberosPrincipal; } } return true; } /** * Test whether the specified configuration is satisfiable by the * given constraint. * * @param config configuration to be tested * @param c the constraint to be tested * @return true if the specified configuration is allowed by * the given constraint, false otherwise. */ static boolean isSatisfiable(Config config, InvocationConstraint c) { /* Note that though some of the checks done here have already been done in isSupportedConstraint(c), they have to be repeated here to support ConstraintAlternatives. */ if (c instanceof ConstraintAlternatives) { Set elems = ((ConstraintAlternatives) c).elements(); for (Iterator iter = elems.iterator(); iter.hasNext(); ) { InvocationConstraint elem = (InvocationConstraint) iter.next(); if (isSatisfiable(config, elem)) return true; } return false; } if (!isSupportedConstraintType(c)) return false; // unsupported constraint type if (c instanceof Integrity) { return c == Integrity.YES; } else if (c instanceof Confidentiality) { return config.encry == (c == Confidentiality.YES); } else if (c instanceof ClientAuthentication) { return c == ClientAuthentication.YES; } else if (c instanceof ServerAuthentication) { return c == ServerAuthentication.YES; } else if (c instanceof Delegation) { return config.deleg == (c == Delegation.YES); } else if (c instanceof ClientMinPrincipal) { Set elems = ((ClientMinPrincipal) c).elements(); if (elems.size() > 1) { return false; // can only authenticate as one principal } else { // elems contains at least one element return elems.contains(config.clientPrincipal); } } else if (c instanceof ClientMinPrincipalType) { Set elems = ((ClientMinPrincipalType) c).elements(); if (elems.size() > 1) { return false; // can only support one type } else { // elems contains at least one element return elems.contains(KerberosPrincipal.class); } } else if (c instanceof ClientMaxPrincipal) { Set elems = ((ClientMaxPrincipal) c).elements(); return elems.contains(config.clientPrincipal); } else if (c instanceof ClientMaxPrincipalType) { Set elems = ((ClientMaxPrincipalType) c).elements(); return elems.contains(KerberosPrincipal.class); } else if (c instanceof ServerMinPrincipal) { Set elems = ((ServerMinPrincipal) c).elements(); if (elems.size() > 1) { return false; // can only authenticate as one principal } else { // elems contains at least one element return elems.contains(config.serverPrincipal); } } return true; } /** * Collect all client principal candidates from the given * constraint. This method assumes homogeneous alternatives. * * @param c the given constraint * @param cpCandidates the set of candidates satisfiable by the * constraints previously checked, which new principals should be * added to. This set contains no principals if no client * principal constraint has been checked yet. * @return false if the passed in constraint is {@link * ClientMinPrincipal} or {@link ClientMaxPrincipal}, or {@link * ConstraintAlternatives} whose elements are of those types, and * is not satisfiable regarding to the given set of candidates, * true other wise. */ static boolean collectCpCandidates( InvocationConstraint c, Set cpCandidates) { boolean isPrincipalConstraint = false; HashSet cpset = new HashSet(); if (c instanceof ConstraintAlternatives) { Set alts = ((ConstraintAlternatives) c).elements(); for (Iterator iter = alts.iterator(); iter.hasNext(); ) { c = (InvocationConstraint) iter.next(); if (c instanceof ClientMinPrincipal) { isPrincipalConstraint = true; Set elems = ((ClientMinPrincipal) c).elements(); Object cp = elems.iterator().next(); if (elems.size() > 1 || !(cp instanceof KerberosPrincipal)) continue; // constraint unsupportable cpset.add(cp); } else if (c instanceof ClientMaxPrincipal) { isPrincipalConstraint = true; Set elems = ((ClientMaxPrincipal) c).elements(); for (Iterator jter = elems.iterator(); jter.hasNext(); ) { Object elem = jter.next(); if (elem instanceof KerberosPrincipal) cpset.add(elem); } } } } else if (c instanceof ClientMinPrincipal) { isPrincipalConstraint = true; Set elems = ((ClientMinPrincipal) c).elements(); Object cp = elems.iterator().next(); if (elems.size() > 1 || !(cp instanceof KerberosPrincipal)) return false; // constraint unsupportable cpset.add(cp); } else if (c instanceof ClientMaxPrincipal) { isPrincipalConstraint = true; Set elems = ((ClientMaxPrincipal) c).elements(); for (Iterator iter = elems.iterator(); iter.hasNext(); ) { Object elem = iter.next(); if (elem instanceof KerberosPrincipal) cpset.add(elem); } } if (isPrincipalConstraint) { if (cpCandidates.size() == 0) { // this constraint is the 1st principal constraint checked if (cpset.size() > 0) { cpCandidates.addAll(cpset); return true; } else { return false; } } else { // seen other principal constraints before cpCandidates.retainAll(cpset); return cpCandidates.size() > 0; } } else { return true; // no say if not principal constraint } } /** * Check whether the caller has the AuthenticationPermission with * the specified principals and action. * * @param local local principal of the * <code>AuthenticationPermission</code>, cannot be * * <code>null<code>. * @param peer peer principal of the * <code>AuthenticationPermission</code>. * @param action action of the * <code>AuthenticationPermission</code>, valid values * include: * "connect", "delegate", "listen", and * "accept". * @throws SecurityException if the caller does not have the * checked permission */ static void checkAuthPermission(KerberosPrincipal local, KerberosPrincipal peer, String action) { SecurityManager sm = System.getSecurityManager(); if (sm != null) { Set localps = Collections.singleton(local); Set peerps = null; if (peer != null) peerps = Collections.singleton(peer); AuthenticationPermission perm = new AuthenticationPermission(localps, peerps, action); sm.checkPermission(perm); } } /** * Check whether the caller has the specified * AuthenticationPermission. * * @param perm the AuthenticationPermission to be checked * @throws SecurityException if the caller does not have the * checked permission */ static void checkAuthPermission(AuthenticationPermission perm) { SecurityManager sm = System.getSecurityManager(); if (sm != null) sm.checkPermission(perm); } /** * Check whether the given set of constraints contains the * candidate constraint. * * @param constraints the constraints to be checked * @param candidate candidate constraint * @return true if the candidate constraint is found in the give * set of constraints, false otherwise. */ static boolean containsConstraint( Set constraints, InvocationConstraint candidate) { for (Iterator iter = constraints.iterator(); iter.hasNext(); ) { InvocationConstraint c = (InvocationConstraint) iter.next(); if (c instanceof ConstraintAlternatives) { Set elems = ((ConstraintAlternatives) c).elements(); return elems.contains(candidate); } else if (c.equals(candidate)) { return true; } } return false; } /** * Get the GSSCredential corresponding to the given principal from * the given <code>Subject</code>, whose usage type is governed by * the usage parameter. * * @param subj the subject from which the TGT or * <code>KerberosKey</code> will be extracted to construct * the GSSCredential, can not be null * @param principal the principal whose name will be used to * construct the GSSCredential. If <code>null</code>, then * a <code>null</code> name will be passed to the * <code>manager</code> to allow it to choose a default. * @param manager the GSSManager instance that will be used to * construct the GSSCredential, can not be null * @param usage intended usage for the GSScredential. The value of * this parameter must be one of: {@link * GSSCredential.INITIATE_AND_ACCEPT}, {@link * GSSCredential.ACCEPT_ONLY}, and {@link * GSSCredential.INITIATE_ONLY}. * @return the requested GSSCredential * @throws UnsupportedConstraintException if failed to get the * requested <code>GSSCredential</code> */ static GSSCredential getGSSCredential ( final Subject subj, final KerberosPrincipal principal, final GSSManager manager, final int usage) throws GSSException { try { return (GSSCredential) Subject.doAs( subj, new PrivilegedExceptionAction() { public Object run() throws GSSException { GSSName name = manager.createName(principal.getName(), krb5NameType); return manager.createCredential( name, GSSCredential.INDEFINITE_LIFETIME, krb5MechOid, usage); } }); } catch (PrivilegedActionException pe) { throw (GSSException) pe.getException(); } } /** * Only throw non-generic exception if caller has getSubject * permission. * * @param detailedException the real * <code>UnsupportedConstraintException</code> or * <code>SecurityException</code> to be thrown if caller * has the "getSubject" <code>AuthPermission</code>. * @param genericException the generic * <code>UnsupportedConstraintException</code> to be thrown * if caller does not have the "getSubject" * <code>AuthPermission</code>. */ static void secureThrow(Exception detailedException, UnsupportedConstraintException genericException) throws UnsupportedConstraintException { if (KerberosUtil.canGetSubject()) { // has "getSubject" permission if (detailedException instanceof SecurityException) { throw (SecurityException) detailedException; } else { throw (UnsupportedConstraintException) detailedException; } } else { throw genericException; } } /** * Logs a throw. Use this method to log a throw when the log * message needs parameters. * * @param logger logger to log to * @param level the log level * @param sourceClass class where throw occurred * @param sourceMethod name of the method where throw occurred * @param msg log message * @param params log message parameters * @param e exception thrown */ static void logThrow(Logger logger, Level level, Class sourceClass, String sourceMethod, String msg, Object[] params, Throwable e) { LogRecord r = new LogRecord(level, msg); r.setLoggerName(logger.getName()); r.setSourceClassName(sourceClass.getName()); r.setSourceMethodName(sourceMethod); r.setParameters(params); r.setThrown(e); logger.log(r); } //----------------------------------- // package-private sub-classes //----------------------------------- /** * An instances of this class records one configuration possibly * satisfiable by this provider. */ static final class Config { /** client principal of the connection */ KerberosPrincipal clientPrincipal; /** server principal of the connection */ KerberosPrincipal serverPrincipal; /** whether the channel should be encrypted */ boolean encry; /** whether client credential should be delegated to server */ boolean deleg; /** number of preferences this config can satisfy */ int prefCount; Config(KerberosPrincipal clientPrincipal, KerberosPrincipal serverPrincipal, boolean encry, boolean deleg) { this.clientPrincipal = clientPrincipal; this.serverPrincipal = serverPrincipal; this.encry = encry; this.deleg = deleg; } /** Returns a string representation of this configuration. */ public String toString() { return "Config[clientPrincipal=" + clientPrincipal + " serverPrincipal=" + serverPrincipal + " encry=" + encry + " deleg=" + deleg + " prefCount=" + prefCount + "]"; } } /** An iterator returns all possible configs */ static final class ConfigIter { private final Set clientPrincipals; private final KerberosPrincipal serverPrincipal; private Iterator cpIter; private final boolean canDeleg; // true if delegation allowed private int configId; private int numConfigs; ConfigIter(Set clientPrincipals, KerberosPrincipal serverPrincipal, boolean canDeleg) { this.clientPrincipals = clientPrincipals; this.serverPrincipal = serverPrincipal; this.canDeleg = canDeleg; configId = 0; numConfigs = clientPrincipals.size() * 2; if (canDeleg) numConfigs *= 2; } boolean hasNext() { return configId < numConfigs; } Config next() { if (configId >= numConfigs) throw new java.util.NoSuchElementException(); if (configId % clientPrincipals.size() == 0) cpIter = clientPrincipals.iterator(); KerberosPrincipal cp = (KerberosPrincipal) cpIter.next(); int encryId = (configId / clientPrincipals.size()) % 2; Config config; if (canDeleg) { int delegId = configId / clientPrincipals.size() / 2; config = new Config(cp, serverPrincipal, BOOL_TABLE[encryId], BOOL_TABLE[delegId]); } else { config = new Config(cp, serverPrincipal, BOOL_TABLE[encryId], false); } ++configId; return config; } } /** * Connection class serves as the parent of connection classes * defined in both client and server end point classes. */ static class Connection { /* 2 means "integrity using DES MAC of MD5 of plaintext" */ protected static final int INTEGRITY_QOP = 2; /* for privacy only the default (0) is supported */ protected static final int PRIVACY_QOP = 0; /** TCP socket used by this connection */ protected final Socket sock; /** Input stream provided by the underlying socket */ protected DataInputStream dis; /** Output stream provided by the underlying socket */ protected DataOutputStream dos; /** client principal of this connection */ KerberosPrincipal clientPrincipal; // serverPrincipal is in endpoint /** * GSSContext instance used by this connection, it is * initialized in child class */ protected GSSContext gssContext; /** Boolean to indicate whether traffic will be encrypted */ protected boolean doEncryption; /** * If this field is set to true, the initiator's credentials * will be delegated to the acceptor during GSS context * establishment. */ protected boolean doDelegation; /** logger of the connection */ protected Logger connectionLogger; /** * Construct a connection object. * * @param sock underlying socket used by this connection */ Connection(Socket sock) throws IOException { this.sock = sock; dis = new DataInputStream(sock.getInputStream()); dos = new DataOutputStream(sock.getOutputStream()); } /** Close the connection */ public void close() { connectionLogger.log(Level.FINE, "closing {0}", this); try { sock.close(); } catch (IOException e) {} } /** * Wrap the content of the buffer into a GSS token and write * it out to the underlying socket. * * @param buf the buffer whose content will be send out * @param offset offset marks the start of the content to be * sent out * @param len number of bytes to be sent out * @throws IOException if problems encountered */ void write(byte[] buf, int offset, int len) throws IOException { MessageProp prop; if (doEncryption) { prop = new MessageProp(PRIVACY_QOP, true); } else { // 2 means "integrity using DES MAC of MD5 of plaintext" prop = new MessageProp(INTEGRITY_QOP, false); } byte[] token = null; try { try { synchronized (gssContext) { token = gssContext.wrap(buf, offset, len, prop); } } catch (GSSException ge) { IOException ioe = new IOException( "Failed to wrap buf into GSS token."); ioe.initCause(ge); throw ioe; } if (doEncryption != prop.getPrivacy()) { throw new IOException( "Returned token encryption property is: " + prop.getPrivacy() + ",\nwhile connection " + "encryption requirement is: " + doEncryption); } if (connectionLogger.isLoggable(Level.FINEST)) { connectionLogger.log( Level.FINEST, "wrapped " + len + " bytes (" + (doEncryption ? "" : "not ") + "encrypted) " + "into a " + token.length + " bytes token and " + "sending it over the network"); } dos.writeInt(token.length); dos.write(token); } catch (IOException ioe) { if (connectionLogger.isLoggable(Levels.FAILED)) { logThrow(connectionLogger, Levels.FAILED, this.getClass(), "write", "failed to wrap buf of size {0} into a GSS " + "token,\nconnection is {1},\nthrows ", new Object[] {new Integer(len), this}, ioe); } throw ioe; } } /** Flush the output stream used for send. */ void flush() throws IOException { dos.flush(); } /** * Block until a complete GSS token has been received, unwrap * it, and return its content. * * @return byte array of the unwrapped GSS token * @throws IOException if problems encountered */ byte[] read() throws IOException { try { MessageProp prop = new MessageProp(0, false); byte[] token = new byte[dis.readInt()]; dis.readFully(token); byte[] bytes; try { synchronized (gssContext) { bytes = gssContext.unwrap( token, 0, token.length, prop); } } catch (GSSException e) { IOException ioe = new IOException( "Failed to unwrap a GSS token of length " + token.length); ioe.initCause(e); throw ioe; } /* this state of the connection can changed by every incoming token */ doEncryption = prop.getPrivacy(); if (connectionLogger.isLoggable(Level.FINEST)) { connectionLogger.log( Level.FINEST, "received a " + token.length + " bytes token (" + (doEncryption ? "" : "not ") + "encrypted), " + bytes.length + " bytes when " + "unwrapped"); } return bytes; } catch (IOException ioe) { if (connectionLogger.isLoggable(Levels.FAILED)) { logThrow(connectionLogger, Levels.FAILED, this.getClass(), "read", "read fails on connection {0}, throws", new Object[] {this}, ioe); } throw ioe; } } } /** * Input stream returned by getInputStream() of client or server * connection */ static class ConnectionInputStream extends InputStream { private byte[] buf; private int offset; // point to the byte for next read private final Connection connection; /** Construct the input stream */ ConnectionInputStream(Connection connection) { buf = new byte[0]; // indicate no buffered data available offset = 0; this.connection = connection; } // This method's javadoc is inherited from InputStream public synchronized int read() throws IOException { if (offset == buf.length) { do { buf = connection.read(); } while (buf.length == 0); offset = 0; } return buf[offset++]; } // This method's javadoc is inherited from InputStream public synchronized int read(byte b[], int off, int len) throws IOException { if (b == null) { throw new NullPointerException(); } else if (off < 0 || len < 0 || (off + len) > b.length) { throw new IndexOutOfBoundsException(); } if (offset == buf.length) { do { buf = connection.read(); } while (buf.length == 0); offset = 0; } int bytes = Math.min(buf.length - offset, len); System.arraycopy(buf, offset, b, off, bytes); offset += bytes; return bytes; } // This method's javadoc is inherited from InputStream public synchronized int available() throws IOException { return buf.length - offset; } /** Close the DataInputStream of the enclosed connection */ public void close() throws IOException { connection.dis.close(); } } /** * Output stream returned by getOutputStream() of client or server * connection */ static class ConnectionOutputStream extends OutputStream { // 8k is chosen because it might be close to a page size private static final int bufSize = 8000; // buf + overhead < 8192 ? private final byte[] buf; private int curLen; // current content length of the internal buffer private final Connection connection; /** Construct an instance of ConnectionOutputStream */ ConnectionOutputStream(Connection connection) { buf = new byte[bufSize]; curLen = 0; // init buf as empty this.connection = connection; } // This method's javadoc is inherited from OutStream public synchronized void write(int b) throws IOException { if (curLen == bufSize) { connection.write(buf, 0, curLen); curLen = 0; } buf[curLen++] = (byte) b; } // This method's javadoc is inherited from OutStream public synchronized void write(byte[] b, int off, int len) throws IOException { if (b == null) { throw new NullPointerException(); } else if (off < 0 || len < 0 || (off + len) > b.length) { throw new IndexOutOfBoundsException(); } if ((curLen + len) >= bufSize) { int count = bufSize - curLen; System.arraycopy(b, off, buf, curLen, count); off += count; len -= count; connection.write(buf, 0, bufSize); curLen = 0; } while (len > bufSize) { connection.write(b, off, bufSize); off += bufSize; len -= bufSize; } System.arraycopy(b, off, buf, curLen, len); curLen += len; } // This method's javadoc is inherited from OutStream public synchronized void flush() throws IOException { if (curLen > 0) { connection.write(buf, 0, curLen); curLen = 0; } connection.flush(); } /** * Flush this stream, then close the DataOutputStream of the * enclosed connection. */ public void close() throws IOException { try { flush(); } finally { connection.dis.close(); } } } /** * A synchronized hash map that only maintains soft reference to * its value objects. It can be configured to have a limited * capacity. LRU replacement policy is used when capacity limit is * reached. */ static class SoftCache { /** Internal hash map used to store the actual key value pairs */ private final LRUHashMap hash; /** Reference queue for cleared ValueCells */ private ReferenceQueue queue = new ReferenceQueue(); /** * Construct an instance of the SoftCache, using a default * capacity of 8. */ SoftCache() { this(Integer.MAX_VALUE, 8); // init cache size as unlimited } /** * Construct an instance of the SoftCache with the given * size limit. * * @param maxCacheSize maximum number of entries allowed in * this cache */ SoftCache(int maxCacheSize) { this(maxCacheSize, 8); } /** * Construct an instance of the SoftCache with the given * size limit and initial capacity. * * @param maxCacheSize maximum number of entries allowed in * this cache * @param initialCapacity initial capacity of the cache */ SoftCache(int maxCacheSize, int initialCapacity) { hash = new LRUHashMap(maxCacheSize, initialCapacity); } /** * Associates the specified value with the specified key in * this cache. Only a soft reference is maintained to each * value object stored in the cache. If the map previously * contained a mapping for this key, and the old value object * has not been garbage collected, the old value will be * returned to the caller. This method is synchronized. * * @param key key with which the specified value is to be * associated. * @param value - value to be associated with the specified * key, only a soft reference is maintained to value in * this cache. * @return previous value associated with specified key, if an * old mapping exists and the old value has not been * garbage collected, or null if there was no * mapping for key. A null return can also indicate * that the HashMap previously associated null with * the specified key. */ public synchronized Object put(Object key, Object value) { processQueue(); ValueCell vc = ValueCell.create(key, value, queue); return ValueCell.strip(hash.put(key, vc), true); } /** * If there is a mapping in this cache for the specified key, * and the softly referenced value of the mapping has not been * garbage collected, return the value, other wise, return * null. This method is synchronized. * * @param key - key whose associated value is to be returned. * @return the value to which this map maps the specified key. */ public synchronized Object get(Object key) { processQueue(); return ValueCell.strip(hash.get(key), false); } /** * Removes the mapping for this key from this cache if it * still exists. This method is synchronized. * * @param key key whose mapping is to be removed from the cache. * @return previous value associated with the specified key * that has not been garbage collected, or null. A * null return can also indicate that the HashMap * previously associated null with the specified key. */ public synchronized Object remove(Object key) { processQueue(); return ValueCell.strip(hash.remove(key), true); } /** * Removes all entries in this cache. This method is * synchronized. */ public synchronized void clear() { processQueue(); hash.clear(); } /** Process the internal ReferenceQueue */ private void processQueue() { ValueCell vc; while ((vc = (ValueCell) queue.poll()) != null) { /* * vc.isValid() is false, then the vc has been * dropped, and the value cell currently in hash * corresponding to vc.key, if exists, must be one * that has been newly inserted using the same key => * can not do hash.remove(vc.key) */ if (vc.isValid()) hash.remove(vc.key); } } /** A linked hash map that implements LRU replacement policy */ private class LRUHashMap extends LinkedHashMap { private int maxCacheSize; /** * Construct an instance of the hash map. * * @param maxCacheSize maximum number of entries allowed * in this map * @param initialCapacity initial capacity of the map * @throws IllegalArgumentException if maxCacheSize is * negative */ LRUHashMap(int maxCacheSize, int initialCapacity) { super(initialCapacity, 0.75f, true); // using access-order if (maxCacheSize < 0) throw new IllegalArgumentException("negative cache size"); this.maxCacheSize = maxCacheSize; } // This method's javadoc is inherited from LinkedHashMap protected boolean removeEldestEntry(Map.Entry eldest) { if (size() > maxCacheSize) { /* * clear the soft ref and mark internal key as * invalid, LRUHashMap will take care of the * removing part */ ValueCell.strip(eldest.getValue(), true); return true; } return false; } } /** * An instance of this class maintains a reference to a key, * and a soft reference to the value the key maps to. */ private static class ValueCell extends SoftReference { static private Object INVALID_KEY = new Object(); private Object key; private ValueCell(Object key, Object value, ReferenceQueue queue) { super(value, queue); this.key = key; } private static ValueCell create(Object key, Object value, ReferenceQueue queue) { if (value == null) return null; return new ValueCell(key, value, queue); } /** * Extract the encapsulated value if the passed in object * is an instance of ValueCell, clear the soft reference * and mark the cell as invalid if drop is true. */ private static Object strip(Object val, boolean drop) { if (val == null) return null; ValueCell vc = (ValueCell)val; Object o = vc.get(); if (drop) vc.drop(); return o; } /** * Return true if this cell has not been dropped, false * otherwise */ private boolean isValid() { return (key != INVALID_KEY); } /** Clear the soft reference, and mark the cell as invalid */ private void drop() { clear(); key = INVALID_KEY; } } } }