/*
* $Id: Association.java 5 2007-03-09 23:22:30Z gremid $
*
* Copyright (c) 2007 the respective author(s) -- see javadoc.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program 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 for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
package net.sf.jz3950;
import net.sf.jz3950.auth.AuthenticationMethod;
import net.sf.jz3950.auth.SimpleAuthenticationMethod;
import net.sf.jz3950.protocol.io.ProtocolInputThread;
import net.sf.jz3950.protocol.io.ProtocolOutputThread;
import net.sf.jz3950.protocol.operation.CloseOperation;
import net.sf.jz3950.protocol.operation.InitOperation;
import net.sf.jz3950.protocol.operation.Operation;
import net.sf.jz3950.protocol.operation.PresentOperation;
import net.sf.jz3950.protocol.operation.SearchOperation;
import net.sf.jz3950.query.Query;
import net.sf.jz3950.record.Record;
import net.sf.jz3950.record.decoder.MarcRecordDecoder;
import net.sf.jz3950.record.decoder.RecordDecoder;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.io.IOException;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Association
{
private List<RecordDecoder> recordDecoders;
private Lock concurrentOperationsLock = new ReentrantLock();
private Log logger;
private ProtocolInputThread inputThread;
private ProtocolOutputThread outputThread;
private Socket connection;
private String associationReferenceId;
private String host;
private TargetInformation targetInformation = new TargetInformation();
private int clientTimeout = 30;
private int port;
private int referenceIdCount;
private int serverTimeout = 3;
public Association()
{
registerRecordDecoders();
}
private void registerRecordDecoders()
{
this.recordDecoders = new ArrayList<RecordDecoder>();
this.recordDecoders.add(new MarcRecordDecoder());
}
public void connect(String host, int port) throws IOException, ProtocolException
{
connect(host, port, SimpleAuthenticationMethod.NONE);
}
public void connect(String host, int port, String user, String password) throws IOException, ProtocolException
{
AuthenticationMethod auth = new SimpleAuthenticationMethod(AuthenticationMethod.AUTH_TYPE_ID_PASS, user, null, password);
connect(host, port, auth);
}
@SuppressWarnings("unchecked")
public void connect(String host, int port, AuthenticationMethod authenticationMethod) throws IOException, ProtocolException
{
if (isRunning())
{
throw new ProtocolException("Already connected and running");
}
this.referenceIdCount = 0;
this.host = host;
this.port = port;
this.logger = LogFactory.getLog(toString() + "[" + getTarget() + "]");
/*
* Connecting to remote host
*/
logger.debug("Connecting to " + getTarget());
this.connection = new Socket(this.host, this.port);
this.connection.setSoTimeout(this.serverTimeout * 1000);
this.inputThread = new ProtocolInputThread("<--" + getTarget(), this.connection.getInputStream());
this.outputThread = new ProtocolOutputThread("-->" + getTarget(), this.connection.getOutputStream());
/*
* INIT-Handshake
*/
try
{
logger.debug("Initializing association");
InitOperation initHandler = new InitOperation(this, authenticationMethod);
requestResponse(initHandler);
this.targetInformation = initHandler.getTargetInformation();
this.associationReferenceId = initHandler.getReferenceId();
logger.debug("Association " + ((this.associationReferenceId == null) ? "" : ("(" + this.associationReferenceId + ") ")) + "initialized (" + this.targetInformation + ")");
}
catch (ProtocolException e)
{
disconnect("INIT error");
throw e;
}
}
public void disconnect(String message) throws ProtocolException
{
checkConnection();
logger.debug("Disconnecting association with " + getTarget());
try
{
request(new CloseOperation(this, this.associationReferenceId, message));
}
catch (ProtocolException e)
{
logger.debug("Ignoring protocol error while closing association", e);
}
this.outputThread.shutdown();
this.inputThread.shutdown();
try
{
if (!this.connection.isClosed())
{
this.connection.close();
}
}
catch (IOException e)
{
logger.warn("I/O error while closing connection to " + getTarget(), e);
}
this.outputThread = null;
this.inputThread = null;
this.connection = null;
this.port = 0;
this.host = null;
}
public void disconnect() throws ProtocolException
{
disconnect("Normal association shutdown");
}
public RecordResultSet search(Query query) throws ProtocolException
{
logger.debug("Search on target with " + query);
checkConnection();
SearchOperation searchProtocolHandler = new SearchOperation(this, query);
requestResponse(searchProtocolHandler);
return new RecordResultSet(this, generateResultSetName(), searchProtocolHandler.getNumOfResults(), searchProtocolHandler.getNextResult());
}
protected List<Record> fetch(String resultSetName, int nextResultNum, int fetchSize) throws ProtocolException
{
logger.debug("Fetching " + fetchSize + " record(s) for result set \"" + resultSetName + "\"; offset: " + (nextResultNum - 1));
checkConnection();
PresentOperation presentProtocolHandler = new PresentOperation(this, resultSetName, nextResultNum, fetchSize);
requestResponse(presentProtocolHandler);
return presentProtocolHandler.getRecords();
}
public String getTarget()
{
return this.host + ":" + this.port;
}
public boolean isRunning()
{
return ((this.inputThread != null) && inputThread.isAlive()) && ((this.outputThread != null) && outputThread.isAlive());
}
private void request(Operation protocolHandler) throws ProtocolException
{
synchronized (protocolHandler)
{
try
{
this.outputThread.addOutgoingOperation(protocolHandler);
protocolHandler.wait(this.clientTimeout * 1000);
}
catch (InterruptedException e)
{
}
finally
{
if (protocolHandler.hasError())
{
throw protocolHandler.getError();
}
if (!protocolHandler.initializingRequestHasBeenSent())
{
throw new ProtocolException("Client timeout while sending request");
}
}
}
}
private void requestResponse(Operation protocolHandler) throws ProtocolException
{
if (!this.targetInformation.isSupportingConcurrentOperations())
{
concurrentOperationsLock.lock();
}
try
{
synchronized (protocolHandler)
{
request(protocolHandler);
try
{
this.inputThread.addPendingOperation(protocolHandler);
protocolHandler.wait(this.clientTimeout * 1000);
}
catch (InterruptedException e)
{
}
finally
{
if (protocolHandler.hasError())
{
throw protocolHandler.getError();
}
if (!protocolHandler.terminatingResponseReceived())
{
throw new ProtocolException("Client timeout while waiting for response");
}
}
}
}
finally
{
if (!this.targetInformation.isSupportingConcurrentOperations())
{
concurrentOperationsLock.unlock();
}
}
}
private void checkConnection() throws ProtocolException
{
if (!isRunning())
{
throw new ProtocolException("Not connected");
}
}
public String generateReferenceId()
{
synchronized (this)
{
return "REF_ID_" + this.referenceIdCount++;
}
}
public String generateResultSetName()
{
// TODO: support multiple result sets
return "default";
}
public List<RecordDecoder> getRecordDecoders()
{
return recordDecoders;
}
}