All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.xsocket.connection.http.client.HttpClient Maven / Gradle / Ivy

There is a newer version: 2.0-beta-1
Show newest version
/*
 *  Copyright (c) xsocket.org, 2006 - 2008. All rights reserved.
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.
 *
 *  This library 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
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * Please refer to the LGPL license at: http://www.gnu.org/copyleft/lesser.txt
 * The latest copy of this software may be found on http://www.xsocket.org/
 */
package org.xsocket.connection.http.client;

import java.io.Closeable;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.net.ssl.SSLContext;


import org.xsocket.ILifeCycle;
import org.xsocket.connection.IConnectionPool;
import org.xsocket.connection.INonBlockingConnection;
import org.xsocket.connection.NonBlockingConnection;
import org.xsocket.connection.NonBlockingConnectionPool;
import org.xsocket.connection.http.BodyDataSink;
import org.xsocket.connection.http.NonBlockingBodyDataSource;
import org.xsocket.connection.http.Request;
import org.xsocket.connection.http.RequestHeader;
import org.xsocket.connection.http.Response;
import org.xsocket.connection.http.ResponseHeader;
import org.xsocket.connection.http.client.HttpClientConnection.BlockingResponseHandler;
import org.xsocket.connection.http.client.HttpClientConnection.ResponseHandlerAdapter;



/**
 * Higher level client-side abstraction of the client side endpoint. The HttpClient uses am internal pool 
 * of {@link HttpClientConnection} to perform the requests. 
 *
 * @author [email protected]
 */
public final class HttpClient implements IHttpClientEndpoint, IConnectionPool, Closeable {
	
	private static final Logger LOG = Logger.getLogger(HttpClient.class.getName());
	
	private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss");
	
	private static final boolean DEFAULT_IS_TRANSACTION_LOG = true;
	public static final int DEFAULT_MAX_REDIRECTS = 5;
	public static final boolean DEFAULT_TREAT_302_REDIRECT_AS_303 = false;
	public static final Integer DEFAULT_RECEIVE_TIMEOUT_MILLIS = Integer.MAX_VALUE;
	
	private int maxRedirects = DEFAULT_MAX_REDIRECTS;
	private boolean isTreat302RedirectAs303 = DEFAULT_TREAT_302_REDIRECT_AS_303;
	private int receiveTimeoutMillis = DEFAULT_RECEIVE_TIMEOUT_MILLIS;
	private boolean isTransactionLog =  DEFAULT_IS_TRANSACTION_LOG;

	private SSLContext sslCtx = null;
	private NonBlockingConnectionPool pool = null;
	
	private boolean isPooled = true;
	private boolean isSSLSupported = false;
	
	
	
	// statistics
	private TransactionLog transactionLog = new TransactionLog(20);
	
	

	/**
	 * constructor 
	 */
	public HttpClient() {
		this(null);
	}
	

	
	/**
	 * constructor 
	 * 
	 * @param sslCtx   the ssl context to use
	 */
	public HttpClient(SSLContext sslCtx) {
		if (sslCtx != null) {
			this.sslCtx = sslCtx;
			pool = new NonBlockingConnectionPool(sslCtx);
			isSSLSupported = true;
		} else {
			pool = new NonBlockingConnectionPool();
			isSSLSupported = false;
		}	
	}


	/**
	 * set the max redirects
	 * 
	 * @param maxRedirects  the max redirects 
	 */
	public void setMaxRedirects(int maxRedirects) {
		this.maxRedirects = maxRedirects;
	}
	
	
	/**
	 * get the max redirects 
	 * 
	 * @return the max redirects
	 */
	public int getMaxRedirects() {
		return maxRedirects;
	}
	
	
	/**
	 * sets if a 302 response should be treat as a 303 response
	 *  
	 * @param isTreat303RedirectAs302 true, if a 303 response should be treat a a 303 response 
	 */
	public void setTreat302RedirectAs303(boolean isTreat303RedirectAs302) {
		this.isTreat302RedirectAs303 = isTreat303RedirectAs302; 
	}
	
	
	/**
	 * gets if a 302 response should be treat as a 303 response
	 * 
	 * @return true, if a 302 response should be treat as a 303 response
	 */
	public boolean isTreat302RedirectAs303() {
		return isTreat302RedirectAs303;
	}
	
	
	/**
	 * activates the transaction log
	 *  
	 * @param isTransactionLog true, if transaction log sholud be activated 
	 */
	void setTransactionLog(boolean isTransactionLog) {
		this.isTransactionLog = isTransactionLog;
	}
	
	/**
	 * returns if the transaction log is activated 
	 * 
	 * @return true, if the transaction log is activated
	 */
	boolean isTransactionLog() {
		return isTransactionLog;
	}
	
	
	/**
	 * get the max size of the transaction log
	 * 
	 * @return the max size of the transaction log
	 */
	int getTransactionLogMaxSize() {
		return transactionLog.getMaxSize();
	}
	
	
	/**
	 * sets the max size of the transaction log
	 * 
	 * @param maxSize the max size of the transaction log
	 */
	void setTransactionLogMaxSize(int maxSize) {
		transactionLog.setMaxSize(maxSize);
	}

	
	/**
	 * set the worker pool which will be assigned to the connections for call back handling
	 * @param workerpool the worker pool
	 */
	public void setWorkerpool(Executor workerpool) {
		pool.setWorkerpool(workerpool);
	}

	
	/**
	 * returns is pooling is used
	 *  
	 * @return true, if pooling is used 
	 */
	public boolean isPooled() {
		return isPooled;
	}
	
	
	/**
	 * sets if pooling is used
	 * 
	 * @param isPooled true, if pooling is used
	 */
	public void setPooled(boolean isPooled) {
		this.isPooled = isPooled;
	}
	
	
	/**
	 * {@inheritDoc}
	 */
	public void setResponseTimeoutMillis(int receiveTimeoutMillis) {
		this.receiveTimeoutMillis = receiveTimeoutMillis;
	}
	
	/**
	 * {@inheritDoc}
	 */
	public int getResponseTimeoutMillis() {
		return receiveTimeoutMillis;
	}
	
	

	/**
	 * {@inheritDoc}
	 */
	public void close() throws IOException {
		pool.close();
	}
	
	/**
	 * {@inheritDoc}
	 */
	public boolean isOpen() {
		return pool.isOpen();
	}
	
	
	/**
	 * returns a unique id 
	 * 
	 * @return the id 
	 */
	public String getId() {
		return Integer.toString(this.hashCode());
	}
	

	/**
	 * {@inheritDoc}
	 */
	public void addListener(ILifeCycle listener) {
		pool.addListener(listener);
	}
	
	/**
	 * {@inheritDoc}
	 */
	public boolean removeListener(ILifeCycle listener) {
		return pool.removeListener(listener);
	}
	
	/**
	 * {@inheritDoc}
	 */
	public void setPooledIdleTimeoutSec(int idleTimeoutSec) {
		pool.setPooledIdleTimeoutSec(idleTimeoutSec);
	}
	
	/**
	 * {@inheritDoc}
	 */
	public int getPooledIdleTimeoutSec() {
		return pool.getPooledIdleTimeoutSec();
	}
	
	/**
	 * {@inheritDoc}
	 */
	public void setPooledLifeTimeoutSec(int lifeTimeoutSec) {
		pool.setPooledLifeTimeoutSec(lifeTimeoutSec);
	}
	
	/**
	 * {@inheritDoc}
	 */
	public int getPooledLifeTimeoutSec() {
		return pool.getPooledLifeTimeoutSec();
	}
	
	/**
	 * {@inheritDoc}
	 */
	public long getCreationMaxWaitMillis() {
		return pool.getCreationMaxWaitMillis();
	}
	
	/**
	 * {@inheritDoc}
	 */
	public void setCreationMaxWaitMillis(long maxWaitMillis) {
		pool.setCreationMaxWaitMillis(maxWaitMillis);
	}
	
	/**
	 * {@inheritDoc}
	 */
	public void setMaxIdlePooled(int maxIdle) {
		pool.setMaxIdlePooled(maxIdle);
	}
	
	/**
	 * {@inheritDoc}
	 */
	public int getMaxIdlePooled() {
		return pool.getMaxIdlePooled();
	}

	/**
	 * {@inheritDoc}
	 */
	public void setMaxActivePooled(int maxActive) {
		pool.setMaxActivePooled(maxActive);
	}
	
	/**
	 * {@inheritDoc}
	 */
	public int getMaxActivePooled() {
		return pool.getMaxActivePooled();
	}
	
	/**
	 * {@inheritDoc}
	 */
	public int getNumPooledActive() {
		return pool.getNumPooledActive();
	}
	
	/**
	 * {@inheritDoc}
	 */
	public int getNumPooledIdle() {
		return pool.getNumPooledIdle();
	}
	
	/**
	 * {@inheritDoc}
	 */
	int getNumPendingGet() {
		return pool.getNumPendingGet();
	}
	
	/**
	 * {@inheritDoc}
	 */
	public int getNumCreated() {
		return pool.getNumCreated();
	}
	
	/**
	 * {@inheritDoc}
	 */
	public int getNumDestroyed() {
		return pool.getNumDestroyed();
	}
	
    /**
	 * {@inheritDoc}
	 */
    public int getNumTimeoutPooledIdle() {
    	return pool.getNumTimeoutPooledIdle();
    }

    /**
	 * {@inheritDoc}
	 */
    public int getNumTimeoutPooledLifetime() {
    	return pool.getNumTimeoutPooledLifetime();
    }
    
	
    /**
	 * {@inheritDoc}
	 */
	public List getActiveConnectionInfos() {
		return pool.getActiveConnectionInfos();
	}
	
	/**
	 * {@inheritDoc}
	 */
	public List getIdleConnectionInfos() {
		return pool.getIdleConnectionInfos();
	}


	/**
	 * returns the transaction log 
	 * @return the transaction log
	 */
	List getTransactionInfos() {
		return transactionLog.getTransactions();
	}
	
	

	
	
	/**
	 * {@inheritDoc}
	 */
	public Response call(Request request) throws IOException, SocketTimeoutException {
			
		// get connection
		HttpClientConnection con = getConnection(request.isSecure(), request.getRemoteHost(), request.getRemotePort());
	
		// create a response handler 
		BlockingAutoCloseResponseHandler responseHandler = new BlockingAutoCloseResponseHandler(con, getResponseTimeoutMillis());
		

		// send the request and wait for the response
		con.send(request, responseHandler);
		Response response = responseHandler.getResponse();
		
		addTransactionInfo(request, response);
		return response;
	}

	

	/**
	 * performs a request, by handling redirects 
	 * 
	 * @param request  the request 
	 * @return the response 
	 * @throws IOException   if an exception occurs 
	 * @throws SocketTimeoutException if the received timeout is exceed
	 */
	public Response callFollowRedirects(Request request) throws IOException, SocketTimeoutException {
		return callFollowRedirects(request, 0);
	}
	

	private Response callFollowRedirects(Request request, int redirectCounter) throws IOException, SocketTimeoutException {

		if (redirectCounter > maxRedirects) {
			throw new IOException("max redirects " + maxRedirects + " reached");
		}
		
		Response response = null;
		

		// duplicate body is redirect is activated
		NonBlockingBodyDataSource copiedBody = null;
		if (request.hasBody()) {
			copiedBody = request.getNonBlockingBody().duplicate();
		}

		
		// get connection and perform call 
		HttpClientConnection con = getConnection(request.isSecure(), request.getRemoteHost(), request.getRemotePort());
		response = con.call(request);
		

		addTransactionInfo(request, response);

		if (isRedirectResponse(request.getRequestHeader(), response.getResponseHeader())) {
			URL newLocation = getRedirectURI(response, request.isSecure(), request.getRemoteHost(), request.getRemotePort());
				
			RequestHeader newRequestHeader = new RequestHeader(request.getMethod(), newLocation.toExternalForm());
			newRequestHeader.copyHeaderFrom(request.getRequestHeader(), "HOST", "CONTENT-LENGTH");
			
			Request newRequest = null;
			if ((response.getStatus() == 303) || ((response.getStatus() == 302) && isTreat302RedirectAs303)) {
				newRequest = new Request(newRequestHeader);
				newRequest.setMethod("GET");
				
			} else {
				newRequest = new Request(newRequestHeader, copiedBody); 
			}
				
			if (LOG.isLoggable(Level.FINE)) {
				LOG.fine("Sending redirect ");
			}
			response = callFollowRedirects(newRequest, ++redirectCounter);
		}
		
		
		return response;
	}
	
	
	
	
	
	/**
	 * {@inheritDoc}
	 */
	public void send(Request request, IResponseHandler responseHandler) throws IOException {

		// get connection
		HttpClientConnection con = getConnection(request.isSecure(), request.getRemoteHost(), request.getRemotePort());

		
		// perform call
		AutoCloseResponseHandlerAdapter responseHandlerAdapter = new AutoCloseResponseHandlerAdapter(responseHandler, con, getResponseTimeoutMillis()); 
		con.send(request, responseHandlerAdapter);
	}
	

	
	/**
	 * {@inheritDoc}
	 */
	public BodyDataSink send(RequestHeader requestHeader, int contentLength, IResponseHandler responseHandler) throws IOException {
		
		// get connection
		HttpClientConnection con = getConnection(requestHeader.isSecure(), requestHeader.getRemoteHost(), requestHeader.getRemotePort());
		
		// perform call
		AutoCloseResponseHandlerAdapter responseHandlerAdapter = new AutoCloseResponseHandlerAdapter(responseHandler, con, getResponseTimeoutMillis());
		return con.sendPlain(requestHeader, contentLength, responseHandlerAdapter);

	}
	
	
	/**
	 * {@inheritDoc}
	 */
	public BodyDataSink send(RequestHeader requestHeader, IResponseHandler responseHandler) throws IOException {
	
		// get connection
		HttpClientConnection con = getConnection(requestHeader.isSecure(), requestHeader.getRemoteHost(), requestHeader.getRemotePort());
		
		// perform call
		return con.sendChunked(requestHeader, new AutoCloseResponseHandlerAdapter(responseHandler, con, getResponseTimeoutMillis()));
	}
	
	
	

	/**
	 * sends a request, by following redirects 
	 * 
	 * @param request           the request 
	 * @param responseHandler   the response handler
	 * @throws IOException if an exception occurs
	 */
	public void sendFollowRedirects(Request request, IResponseHandler responseHandler) throws IOException {
		sendFollowRedirects(request, responseHandler, 0);
	}
	
	
	private void sendFollowRedirects(Request request, IResponseHandler responseHandler, int redirectCounter) throws IOException {

		// duplicate body and create RedirectAdapter is redirect is activated
		NonBlockingBodyDataSource copiedBody = null;
		if (request.hasBody()) {
			copiedBody = request.getNonBlockingBody().duplicate();
		}
	
		
		// get connection
		HttpClientConnection con = getConnection(request.isSecure(), request.getRemoteHost(), request.getRemotePort());

		
		// perform call
		AutoCloseRedirectResponseHandlerAdapter responseHandlerAdapter = new AutoCloseRedirectResponseHandlerAdapter(responseHandler, con, request.getRequestHeader(), copiedBody, redirectCounter, getResponseTimeoutMillis()); 
		con.send(request, responseHandlerAdapter);
	}
	
	
	
	
	

	
	private void addTransactionInfo(Request request, Response response) {
		
		if (isTransactionLog) {
			String info = null;
			if (request.getQueryString() != null) {
				info = "[" + DATE_FORMAT.format(new Date()) + "] " + request.getRemoteHost() + ":" + request.getRemotePort() + 
				       " " + request.getMethod() + " " + request.getRequestURI() + request.getQueryString() + 
				       " -> " + response.getStatus() + " " + response.getReason();
			} else {
				info = "[" + DATE_FORMAT.format(new Date()) + "] " + request.getRemoteHost() + ":" + request.getRemotePort() +
				       " " + request.getMethod() + " " + request.getRequestURI() +
				       " -> " + response.getStatus() + " " + response.getReason();
			}
			
			
			if (response.containsHeader("connection")) {
				info = info + " (connection: " + response.getHeader("connection") + ")";
			}
			
			
			transactionLog.add(info);
		}
	}
	


	

	
	private HttpClientConnection getConnection(boolean isSSL, String host, int port) throws IOException {
		
		if ((isSSL == true) && !isSSLSupported) {
			throw new IOException("ssl connection are not supported (use pool sslContext parameter constructor)");
		}
		
		INonBlockingConnection tcpConnection = null;
		
		if (isPooled) {
			try {
				tcpConnection = pool.getNonBlockingConnection(host, port, isSSL);
			} catch (IOException ioe) {
				throw new IOException("could not retrieve a pooled tcp connection to " + host + ":" + port + " reason: " + ioe.toString());
			}

				
		} else {
			try {
				if (sslCtx != null) {
					tcpConnection = new NonBlockingConnection(host, port, sslCtx, true);
					((NonBlockingConnection) tcpConnection).setWorkerpool(pool.getWorkerpool());
				} else {
					tcpConnection = new NonBlockingConnection(host, port);
					((NonBlockingConnection) tcpConnection).setWorkerpool(pool.getWorkerpool());
				}
			} catch (IOException ioe) {
				throw new IOException("could not establish new tcp connection to " + host + ":" + port + " reason: " + ioe.toString());
			}
		}
		
		HttpClientConnection httpConnection = new HttpClientConnection(tcpConnection);
		httpConnection.setResponseTimeoutMillis(receiveTimeoutMillis);
		return httpConnection;
	}
	
	

	private boolean isRedirectResponse(RequestHeader requestHeader, ResponseHeader responseHeader) {
		
		switch (responseHeader.getStatus()) {
		
		
		// 300 Multiple choices
		case 300:
			return false;
			
		// 301 Moved permanently
		case 301:
			if (requestHeader.getMethod().equalsIgnoreCase("GET") || requestHeader.getMethod().equalsIgnoreCase("HEAD")) {
				return true;
			}
			
			return false;

			
		// 302 found
		case 302:
			if (isTreat302RedirectAs303) {
				return true;
			}
			
			if (requestHeader.getMethod().equalsIgnoreCase("GET") || requestHeader.getMethod().equalsIgnoreCase("HEAD")) {
				return true;
			}
			
			return false;
		
		// 303 See other
		case 303:
			return true;
		
			
		// 304 Not modified
		case 304:
			return false;

		// 305 Use proxy
		case 305:
			return false;

		// 306 (unused)
		case 306:
			return false;

		// 307 temporary redirect
		case 307:
			return false;

			
		default:
			return false;
		}
	}
	
	
	
	private static final URL getRedirectURI(Response response, boolean isSSL, String originalHost, int originalPort) {
		if (response.getStatus() == 302) {
			String location = response.getHeader("Location");
			
			// absolute URL? 
			try {
				return new URL(location);
				
			} catch (MalformedURLException mue) {
				// no
				try {
					if (isSSL) {
						return new URL("https://" + originalHost + ":" + originalPort + location);						
					} else {
						return new URL("http://" + originalHost + ":" + originalPort + location);
					}
				} catch (MalformedURLException e) {
					if (LOG.isLoggable(Level.FINE)) {
						LOG.fine("could not create relocation url . reason " + e.toString());
					}
				}
			}			
		}
	
		return null;
	}
	
	
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder(super.toString());
		
		sb.append("\r\nactive connections:");
		for (String connectionInfo : getActiveConnectionInfos()) {
			sb.append("\r\n " + connectionInfo);
		}
		
		sb.append("\r\nidle connections:");
		for (String connectionInfo : getIdleConnectionInfos()) {
			sb.append("\r\n " + connectionInfo);
		}

		
		sb.append("\r\ntransaction log:");
		for (String transactionInfo : getTransactionInfos()) {
			sb.append("\r\n " + transactionInfo);
		}
		
		return sb.toString();
	}
	
	
	
	
	static class BlockingAutoCloseResponseHandler extends BlockingResponseHandler {
		
		
		public BlockingAutoCloseResponseHandler(HttpClientConnection httpConnection, long maxReceiveTime) {
			super(ClientUtils.RESPONSE_HANDLER_INFO_NONTHREADED_MESSAGE_RECEIVED, httpConnection, maxReceiveTime);
		}
		
		
		@Override
		public void onMessageCompleteReceived() {
			closeConnection();
		}		
	}
	

	

	static class AutoCloseResponseHandlerAdapter extends ResponseHandlerAdapter {
		
		public AutoCloseResponseHandlerAdapter(IResponseHandler responseHandler, HttpClientConnection httpConnection, long maxReceiveTime) throws IOException {
			super(responseHandler, httpConnection, maxReceiveTime);		
		}
		
		
		@Override
		public void onMessageCompleteReceived() {
			closeConnection();
		}		
	}
	
	
	
	class AutoCloseRedirectResponseHandlerAdapter extends ResponseHandlerAdapter {
			
		private IResponseHandler responseHandler = null;
		
		// redirect support
		private int currentRedirects = 0;
		private RequestHeader originalRequestHeader = null;
		private NonBlockingBodyDataSource originalRequestBody = null;
		
		
		public AutoCloseRedirectResponseHandlerAdapter(IResponseHandler responseHandler, HttpClientConnection httpConnection, RequestHeader originalRequestHeader, NonBlockingBodyDataSource originalRequestBody, int currentRedirects, long maxReceiveTime) throws IOException {
			super(responseHandler, httpConnection, maxReceiveTime);		
			this.responseHandler = responseHandler;
			this.originalRequestHeader = originalRequestHeader;
			this.originalRequestBody = originalRequestBody;
			this.currentRedirects = currentRedirects;
		}
		
		
		@Override
		public void performOnResponse(Response response) {
			
			try {
				// redirect handling
				if (isRedirectResponse(originalRequestHeader, response.getResponseHeader())) {			
					URL newLocation = getRedirectURI(response, originalRequestHeader.isSecure(), originalRequestHeader.getRemoteHost(), originalRequestHeader.getRemotePort());
						
					RequestHeader newRequestHeader = new RequestHeader(originalRequestHeader.getMethod(), newLocation.toExternalForm());
					newRequestHeader.copyHeaderFrom(originalRequestHeader, "HOST", "CONTENT-LENGTH");
							
					Request newRequest = null;
					if ((response.getStatus() == 303) || ((response.getStatus() == 302) && isTreat302RedirectAs303)) {
						newRequest = new Request(newRequestHeader);
						newRequest.setMethod("GET");
						
					} else {
						newRequest = new Request(newRequestHeader, originalRequestBody); 
					}
						
					sendFollowRedirects(newRequest, responseHandler, ++currentRedirects);
						
				} else {
					super.performOnResponse(response);
				}
				
			} catch (IOException ioe) {
				if (LOG.isLoggable(Level.FINE)) {
					LOG.fine("error occured by calling on response " + ioe.toString());
				}
			}
		}
		
		
		@Override
		public void onMessageCompleteReceived() {
			closeConnection();
		}		
	}
	
	
	
	
	
	@SuppressWarnings("unchecked")
	private static final class TransactionLog {
		
		private LinkedList transactions = new LinkedList();
		private int maxSize = 0;
		
		TransactionLog(int maxSize) {
			this.maxSize = maxSize;
		}
		
		void setMaxSize(int maxSize) {
			this.maxSize = maxSize;
		}
		
		int getMaxSize() {
			return maxSize;
		}
		
		void add(String transactionInfo) {
			transactions.add(transactionInfo);
			if (transactions.size() > maxSize) {
				try {
					transactions.removeFirst();
				} catch (Exception e) {
					if (LOG.isLoggable(Level.FINE)) {
						LOG.fine("error occured by removing list entry " + e.toString());
					}
				}
			}
		}
		
		
		public List getTransactions() {
			return (List) transactions.clone();
		}

	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy