/*
* 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 org.apache.harmony.jndi.provider.ldap;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import javax.naming.CommunicationException;
import javax.naming.ConfigurationException;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.ldap.Control;
import javax.naming.ldap.StartTlsRequest;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import org.apache.harmony.jndi.internal.nls.Messages;
import org.apache.harmony.jndi.provider.ldap.LdapContextImpl.UnsolicitedListener;
import org.apache.harmony.jndi.provider.ldap.asn1.ASN1Decodable;
import org.apache.harmony.jndi.provider.ldap.asn1.ASN1Encodable;
import org.apache.harmony.jndi.provider.ldap.asn1.LdapASN1Constant;
import org.apache.harmony.jndi.provider.ldap.event.ECNotificationControl;
import org.apache.harmony.jndi.provider.ldap.event.PersistentSearchControl;
import org.apache.harmony.jndi.provider.ldap.event.PersistentSearchResult;
import org.apache.harmony.security.asn1.ASN1Integer;
/**
* LdapClient is the actual class used to communicate with Ldap Server.
*
*/
public class LdapClient {
/**
* Socket used to communicate with Ldap Server.
*/
private Socket socket;
/**
* Input stream of socket.
*/
private InputStream in;
/**
* Output stream of socket.
*/
private OutputStream out;
/**
* Address of connection
*/
private String address;
/**
* port of connection
*/
private int port;
/**
* blocked requests list which wait for response
*/
private Hashtable<Integer, Element> requests = new Hashtable<Integer, Element>();
/**
* the max time to wait server response in milli-second
*/
private long MAX_WAIT_TIME = 30 * 1000;
/**
* responsible for dispatching received messages
*/
private Dispatcher dispatcher;
/**
* registered UnsolicitedListener
*/
private List<UnsolicitedListener> unls = new ArrayList<UnsolicitedListener>();
// constructor for test
public LdapClient() {
// do nothing
}
/**
* Constructor for LdapClient.
*
* @param factory
* used to construct socket through its factory method
* @param address
* the Internet Protocol (IP) address of ldap server
* @param port
* the port number of ldap server
* @throws UnknownHostException
* if the host cannot be resolved
* @throws IOException
* if an error occurs while instantiating the socket
*/
public LdapClient(SocketFactory factory, String address, int port)
throws UnknownHostException, IOException {
this.address = address;
this.port = port;
socket = factory.createSocket(address, port);
// FIXME: Use of InputStreamWrap here is to deal with a potential bug of
// RI.
in = new InputStreamWrap(socket.getInputStream());
out = socket.getOutputStream();
dispatcher = new Dispatcher();
dispatcher.start();
}
/**
* The instance of the class is daemon thread, which read messages from
* server and dispatch to corresponding thread.
*/
class Dispatcher extends Thread {
private boolean isStopped = false;
public Dispatcher() {
/**
* must be daemon thread, otherwrise can't destory by gc
*/
setDaemon(true);
}
public boolean isStopped() {
return isStopped;
}
public void setStopped(boolean isStopped) {
this.isStopped = isStopped;
}
@Override
public void run() {
while (!isStopped) {
try {
// set response op to null, load later
LdapMessage response = new LdapMessage(null) {
/**
* Dispatcher can't know which response operation should
* be used until messageId had determined.
*
* @return response according messageId
*/
@Override
public ASN1Decodable getResponseOp() {
// responseOp has been load, just return it
if (super.getResponseOp() != null) {
return super.getResponseOp();
}
int messageId = getMessageId();
// Unsolicited Notification
if (messageId == 0) {
return new UnsolicitedNotificationImpl();
}
// get response operation according messageId
Element element = requests.get(Integer
.valueOf(messageId));
if (element != null) {
return element.response.getResponseOp();
}
/*
* FIXME: if messageId not find in request list,
* what should we do?
*/
return null;
}
};
Exception ex = null;
/**
* TODO read message data by ourselves then decode, this
* would be robust
*/
try {
// read next message
response.decode(in);
} catch (IOException e) {
// may socket has problem or decode occurs error
ex = e;
} catch (RuntimeException e) {
// may socket has problem or decode occurs error
ex = e;
}
processResponse(response, ex);
} catch (Exception e) {
// may never reach
e.printStackTrace();
}
}
}
private void processResponse(LdapMessage response, Exception ex) {
// unsolicited notification
if (response.getMessageId() == 0) {
notifyUnls(response);
return;
}
Element element = requests.get(Integer.valueOf(response
.getMessageId()));
if (element != null) {
element.response = response;
element.ex = ex;
// persistent search response
if (element.lock == null) {
notifyPersistenSearchListener(element);
} else {
/*
* notify the thread which send request and wait for
* response
*/
if (element.response.getOperationIndex() == LdapASN1Constant.OP_EXTENDED_RESPONSE
&& ((ExtendedOp) element.response.getResponseOp())
.getExtendedRequest().getID().equals(
StartTlsRequest.OID)) {
/*
* When establishing TLS by StartTls extended operation,
* no
*/
isStopped = true;
}
synchronized (element.lock) {
element.lock.notify();
}
} // end of if (element.lock == null) else
} // end of if (element != null)
else if (ex != null) {
/*
* may asn1 decode error or socket problem, can get message id,
* so couldn't know which thread should be notified
*/
// FIXME: any better way?
close();
}
// FIXME message id not found and no exception, what shoud we do?
} // end of processResponse
} // Dispatcher
private void notifyUnls(LdapMessage response) {
UnsolicitedNotificationImpl un = (UnsolicitedNotificationImpl) response
.getResponseOp();
for (UnsolicitedListener listener : unls) {
listener.receiveNotification(un, response.getControls());
}
}
/**
* Carry out the ldap operation encapsulated in operation with controls.
*
* @param operation
* the ldap operation
* @param controls
* extra controls for some ldap operations
* @return the encapsulated response message from ldap server
* @throws IOException
*/
public LdapMessage doOperation(LdapOperation operation, Control[] controls)
throws IOException {
return doOperation(operation.getRequestId(), operation.getRequest(),
operation.getResponse(), controls);
}
/**
* Send out the ldap operation in request with controls, and decode response
* into LdapMessage.
*
* @param opIndex
* @param request
* the ldap request
* @param response
* the ldap response
* @param controls
* extra controls for some ldap operations
* @return the encapsulated response message from ldap server
* @throws IOException
*/
public LdapMessage doOperation(int opIndex, ASN1Encodable request,
ASN1Decodable response, Control[] controls) throws IOException {
if (opIndex == LdapASN1Constant.OP_SEARCH_REQUEST) {
return doSearchOperation(request, response, controls);
}
LdapMessage requestMsg = new LdapMessage(opIndex, request, controls);
Integer messageID = Integer.valueOf(requestMsg.getMessageId());
Object lock = new Object();
requests.put(messageID, new Element(lock, new LdapMessage(response)));
try {
out.write(requestMsg.encode());
out.flush();
return waitResponse(messageID, lock);
} finally {
// remove request from list
requests.remove(messageID);
}
}
/**
* Block the current thread until get response from server or occurs error
*
* @param messageID
* id of request message, is same as id of response message
* @param response
* decoder of the response
* @return response message, may not be null
*
* @throws Exception
*/
private LdapMessage waitResponse(Integer messageID, Object lock)
throws IOException {
Element element = requests.get(messageID);
/*
* test if dispatcher has not received response message from server,
* wait response
*/
if (element.response.getMessageId() != messageID.intValue()) {
synchronized (lock) {
try {
lock.wait(MAX_WAIT_TIME);
} catch (InterruptedException e) {
// ignore
}
}
}
element = requests.get(messageID);
// wait time out
if (element.response.getMessageId() != messageID.intValue()) {
// ldap.31=Read LDAP response message time out
throw new IOException(Messages.getString("ldap.31")); //$NON-NLS-1$
}
// error occurs when read response
if (element.ex != null) {
// socket is not connected
if (!socket.isConnected()) {
close();
}
// element.ex must be one of IOException or RuntimeException
if (element.ex instanceof IOException) {
throw (IOException) element.ex;
}
throw (RuntimeException) element.ex;
}
return element.response;
}
private LdapMessage doSearchOperation(ASN1Encodable request,
ASN1Decodable response, Control[] controls) throws IOException {
LdapMessage requestMsg = new LdapMessage(
LdapASN1Constant.OP_SEARCH_REQUEST, request, controls);
Integer messageID = Integer.valueOf(requestMsg.getMessageId());
Object lock = new Object();
requests.put(messageID, new Element(lock, new LdapMessage(response)));
try {
out.write(requestMsg.encode());
out.flush();
LdapMessage responseMsg = waitResponse(messageID, lock);
while (responseMsg.getOperationIndex() != LdapASN1Constant.OP_SEARCH_RESULT_DONE) {
responseMsg = waitResponse(messageID, lock);
}
return responseMsg;
} finally {
// remove request from list
requests.remove(messageID);
}
}
public void abandon(final int messageId, Control[] controls)
throws IOException {
doOperationWithoutResponse(LdapASN1Constant.OP_ABANDON_REQUEST,
new ASN1Encodable() {
public void encodeValues(Object[] values) {
values[0] = ASN1Integer.fromIntValue(messageId);
}
}, controls);
}
public void doOperationWithoutResponse(int opIndex, ASN1Encodable op,
Control[] controls) throws IOException {
LdapMessage request = new LdapMessage(opIndex, op, controls);
out.write(request.encode());
out.flush();
}
public int addPersistentSearch(SearchOp op) throws IOException {
LdapMessage request = new LdapMessage(
LdapASN1Constant.OP_SEARCH_REQUEST, op.getRequest(),
new Control[] { new PersistentSearchControl() });
Integer messageID = Integer.valueOf(request.getMessageId());
// set lock to null, indicate this is persistent search
requests.put(messageID, new Element(null, new LdapMessage(op
.getResponse())));
try {
out.write(request.encode());
out.flush();
return request.getMessageId();
} catch (IOException e) {
// send request faild, remove request from list
requests.remove(messageID);
throw e;
}
}
public void removePersistentSearch(int messageId, Control[] controls)
throws IOException {
requests.remove(Integer.valueOf(messageId));
abandon(messageId, controls);
}
/**
* Close network connection, stop dispather thread, and release all other
* resources
*
* NOTE: invoke this method should be careful when this
* <code>LdapClient</code> instance is shared by multi
* <code>LdapContext</code>
*
*/
public void close() {
// close socket
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
// ignore
}
}
socket = null;
in = null;
out = null;
// try to stop dispather
if (dispatcher != null) {
dispatcher.setStopped(true);
dispatcher.interrupt();
}
// notify all blocked thread
if (requests != null) {
for (Element element : requests.values()) {
if (element.lock != null) {
synchronized (element.lock) {
element.lock.notify();
}
} else {
// TODO notify persistent search listeners
}
}
requests.clear();
requests = null;
}
}
/**
* Get new instance of LdapClient according environment variable
*
* @param envmt
* @return
* @throws NamingException
*/
public static LdapClient newInstance(String host, int port,
Hashtable<?, ?> envmt) throws NamingException {
String factoryName = (String) envmt
.get("java.naming.ldap.factory.socket");
SocketFactory factory = null;
if (factoryName == null || "".equals(factoryName)) {
if ("ssl".equalsIgnoreCase((String) envmt
.get(Context.SECURITY_PROTOCOL))) {
factory = SSLSocketFactory.getDefault();
} else {
factory = SocketFactory.getDefault();
}
} else {
try {
factory = (SocketFactory) classForName(factoryName)
.newInstance();
} catch (Exception e) {
ConfigurationException ex = new ConfigurationException();
ex.setRootCause(e);
throw ex;
}
}
// TODO: get LdapClient from pool first.
try {
return new LdapClient(factory, host, port);
} catch (IOException e) {
CommunicationException ex = new CommunicationException();
ex.setRootCause(e);
throw ex;
}
}
private static Class<?> classForName(final String className)
throws ClassNotFoundException {
Class<?> cls = null;
// try thread context class loader first
try {
cls = Class.forName(className, true, Thread.currentThread()
.getContextClassLoader());
} catch (ClassNotFoundException e) {
// Ignored.
}
// try system class loader second
try {
cls = Class.forName(className, true, ClassLoader
.getSystemClassLoader());
} catch (ClassNotFoundException e1) {
// Ignored.
}
if (cls == null) {
// jndi.1C=class {0} not found
throw new ClassNotFoundException(Messages.getString(
"jndi.1C", className)); //$NON-NLS-1$
}
return cls;
}
/**
* struct for holding necessary info to add to requests list
*/
static class Element {
Object lock;
LdapMessage response;
Exception ex;
public Element(Object lock, LdapMessage response) {
this.lock = lock;
this.response = response;
}
}
// TODO: This class is used to deal with a potential bug of RI, may be
// removed in the future.
/**
* When use <code>InputStream</code> from SSL Socket, if invoke
* <code>InputStream.read(byte[])</code> with byte array of zero length,
* the method will be blocked. Seems it's bug of ri.
*
* This wrap class delegate all request to wrapped instance, except
* returning immediately when the invoke
* <code>InputStream.read(byte[])</code> with byte array of zero length.
*/
static class InputStreamWrap extends InputStream {
InputStream in;
public InputStreamWrap(InputStream in) {
this.in = in;
}
@Override
public int read() throws IOException {
return in.read();
}
@Override
public int read(byte[] bs, int offset, int len) throws IOException {
if (len == 0) {
return 0;
}
return in.read(bs, offset, len);
}
@Override
public void reset() throws IOException {
in.reset();
}
@Override
public int available() throws IOException {
return in.available();
}
@Override
public void close() throws IOException {
in.close();
}
@Override
public void mark(int readlimit) {
in.mark(readlimit);
}
@Override
public boolean markSupported() {
return in.markSupported();
}
@Override
public int read(byte[] b) throws IOException {
return in.read(b);
}
@Override
public long skip(long n) throws IOException {
return in.skip(n);
}
}
public String getAddress() {
return address;
}
public int getPort() {
return port;
}
@Override
protected void finalize() {
close();
}
public Socket getSocket() {
return this.socket;
}
public void setSocket(Socket socket) throws IOException {
this.socket = socket;
this.in = new InputStreamWrap(socket.getInputStream());
this.out = socket.getOutputStream();
if (dispatcher != null) {
dispatcher.setStopped(true);
dispatcher.interrupt();
}
this.dispatcher = new Dispatcher();
this.dispatcher.start();
}
public void addUnsolicitedListener(UnsolicitedListener listener) {
if (unls == null) {
unls = new ArrayList<UnsolicitedListener>();
}
if (!unls.contains(listener)) {
unls.add(listener);
}
}
private void notifyPersistenSearchListener(Element element) {
PersistentSearchResult psr = (PersistentSearchResult) ((SearchOp) element.response
.getResponseOp()).getSearchResult();
// test error
if (psr.getResult() != null) {
psr.receiveNotificationHook(psr.getResult());
}
// notify listener
Control[] cs = element.response.getControls();
if (cs != null) {
for (int i = 0; i < cs.length; i++) {
Control control = cs[i];
if (ECNotificationControl.OID.equals(control.getID())) {
psr.receiveNotificationHook(new ECNotificationControl(
control.getEncodedValue()));
}
}
}
}
}