/*
* RHQ Management Platform
* Copyright (C) 2005-2008 Red Hat, Inc.
* All rights reserved.
*
* 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 version 2 of the License.
*
* 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., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package org.rhq.enterprise.communications.command.impl.stream.server;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicLong;
import mazz.i18n.Logger;
import org.jboss.remoting.invocation.NameBasedInvocation;
import org.rhq.enterprise.communications.command.Command;
import org.rhq.enterprise.communications.command.CommandExecutor;
import org.rhq.enterprise.communications.command.CommandResponse;
import org.rhq.enterprise.communications.command.CommandType;
import org.rhq.enterprise.communications.command.impl.stream.RemoteInputStreamCommand;
import org.rhq.enterprise.communications.command.impl.stream.RemoteInputStreamCommandResponse;
import org.rhq.enterprise.communications.command.server.CommandMBean;
import org.rhq.enterprise.communications.command.server.CommandService;
import org.rhq.enterprise.communications.command.server.CommandServiceMBean;
import org.rhq.enterprise.communications.i18n.CommI18NFactory;
import org.rhq.enterprise.communications.i18n.CommI18NResourceKeys;
import org.rhq.enterprise.communications.util.ClassUtil;
/**
* Processes client requests to read remoted input streams.
*
* @author John Mazzitelli
*/
public class RemoteInputStreamCommandService extends CommandService {
/**
* Logger
*/
private static final Logger LOG = CommI18NFactory.getLogger(RemoteInputStreamCommandService.class);
/**
* Used for its monitor lock to synchronize access to the maps, timer and index counter.
*/
private final Object m_lock = new Object();
/**
* An index counter that is incremented each time a new stream is added - these index numbers end up being keys to
* the stream map and LAT map and are the stream IDs returned by the add method.
*/
private long m_index;
/**
* The input streams that this service effectively remotes to its clients. The key values are the streams' index
* numbers.
*/
private final Map<Long, InputStream> m_remotedInputStreams;
/**
* The last access times for all the streams - the key values are the streams' index numbers.
*/
private final Map<Long, AtomicLong> m_lastAccessTimes;
/**
* The maximum amount of milliseconds a stream is allowed to be idle before it will be removed and no longer
* accessible to clients.
*/
private long m_maxIdleTime;
/**
* This timer runs tasks that check when an input stream has been idle for a long time and closes/removes those idle
* streams.
*/
private Timer m_idleTimer;
/**
* Constructor for {@link RemoteInputStreamCommandService}.
*/
public RemoteInputStreamCommandService() {
m_index = 0L;
m_remotedInputStreams = new HashMap<Long, InputStream>();
m_lastAccessTimes = new HashMap<Long, AtomicLong>();
m_maxIdleTime = 30000L;
m_idleTimer = null;
}
/**
* Adds the given input stream to this service, effectively allowing remote clients to access this stream. The
* returned value is the ID that clients need to use to identify this stream as the one the client wants to access
* (see {@link RemoteInputStreamCommand#setStreamId(Long)}).
*
* @param stream the new stream to remote
*
* @return the stream's ID
*/
public Long addInputStream(InputStream stream) {
Long stream_id;
AtomicLong lat = new AtomicLong(System.currentTimeMillis());
synchronized (m_lock) {
stream_id = Long.valueOf(++m_index);
m_remotedInputStreams.put(stream_id, stream);
m_lastAccessTimes.put(stream_id, lat);
if (m_idleTimer == null) {
m_idleTimer = new Timer("RHQ Idle Input Stream Timer Thread", true);
}
createIdleTimerTask(stream_id, lat);
}
LOG.debug(CommI18NResourceKeys.ADDED_REMOTE_STREAM, stream_id);
return stream_id;
}
/**
* Removes the stream associated with the given ID from this service, effectively making this stream inaccessible to
* remote clients. This method also ensures the input stream is closed.
*
* @param stream_id identifies the stream to remove
*
* @return <code>true</code> if the stream ID was valid and a stream was removed; <code>false</code> if the ID
* referred to a non-existent stream (which could mean either the stream was never registered at all or it
* was registered but has already been removed)
*/
public boolean removeInputStream(Long stream_id) {
InputStream doomed_stream;
AtomicLong doomed_lat;
synchronized (m_lock) {
doomed_stream = m_remotedInputStreams.remove(stream_id);
doomed_lat = m_lastAccessTimes.remove(stream_id);
if ((m_remotedInputStreams.size() == 0) && (m_idleTimer != null)) {
m_idleTimer.cancel();
m_idleTimer = null;
}
}
// just to be doubly sure we leave no resources hanging around, let's ensure the stream is closed
if (doomed_stream != null) {
try {
doomed_stream.close();
} catch (Throwable t) {
}
LOG.debug(CommI18NResourceKeys.REMOVED_REMOTE_STREAM, stream_id);
}
// set the LAT to quickly force the idle timer task for this stream to exit
if (doomed_lat != null) {
doomed_lat.set(0L);
}
return (doomed_stream != null);
}
/**
* Configures the max idle time taken from this service's container configuration.
*
* @see CommandMBean#startService()
*/
@Override
public void startService() {
super.startService();
m_maxIdleTime = getServiceContainer().getConfiguration().getRemoteStreamMaxIdleTime();
return;
}
/**
* @see CommandMBean#stopService()
*/
@Override
public void stopService() {
super.stopService();
synchronized (m_lock) {
Long[] doomed_ids = m_remotedInputStreams.keySet().toArray(new Long[0]);
for (int i = 0; i < doomed_ids.length; i++) {
removeInputStream(doomed_ids[i]); // also forces the streams to close and eventually kills timer
}
}
return;
}
/**
* Takes the remote stream access request, which has the NameBasedInvocation parameter, and convert that to a method
* call on the target stream (using reflection). Then return the Object returned from the method call on the target
* stream in the response. Note that the invocation signature must match one of the methods on <code>
* InputStream</code>.
*
* @see CommandExecutor#execute(Command, InputStream, OutputStream)
*/
public CommandResponse execute(Command command, InputStream in, OutputStream out) {
RemoteInputStreamCommand remote_command = new RemoteInputStreamCommand(command);
NameBasedInvocation invocation = remote_command.getNameBasedInvocation();
String method_name = invocation.getMethodName();
Object[] params = invocation.getParameters();
String[] signature = invocation.getSignature();
Class<?>[] class_signature = new Class[signature.length];
RemoteInputStreamCommandResponse response;
try {
// get the stream that the command wants to access
Long stream_id = remote_command.getStreamId();
InputStream the_stream;
synchronized (m_lock) {
the_stream = m_remotedInputStreams.get(stream_id);
if (the_stream == null) {
throw new IllegalArgumentException(LOG.getMsgString(CommI18NResourceKeys.INVALID_STREAM_ID,
stream_id, remote_command));
}
setLastAccess(stream_id, System.currentTimeMillis());
}
LOG.debug(CommI18NResourceKeys.INVOKING_STREAM_FROM_REMOTE_CLIENT, stream_id, method_name);
// use reflection to make the call
for (int x = 0; x < signature.length; x++) {
class_signature[x] = ClassUtil.getClassFromTypeName(signature[x]);
}
Method method = InputStream.class.getMethod(method_name, class_signature);
Object results = method.invoke(the_stream, params);
response = new RemoteInputStreamCommandResponse(remote_command, results);
// if the client has asked to close the stream, then we will ask that this service be deregistered as soon as possible
// one the stream is closed, a client should not request anything else from our service (obviously, there is nothing else
// this service can provide since the stream is now useless)
if ("close".equals(method_name)) {
setLastAccess(stream_id, 0L);
}
} catch (Exception e) {
LOG.warn(e, CommI18NResourceKeys.FAILED_TO_INVOKE_STREAM_METHOD, method_name, remote_command);
response = new RemoteInputStreamCommandResponse(remote_command, e);
}
return response;
}
/**
* Supports {@link RemoteInputStreamCommand#COMMAND_TYPE remote input stream commands}.
*
* @see CommandServiceMBean#getSupportedCommandTypes()
*/
public CommandType[] getSupportedCommandTypes() {
return new CommandType[] { RemoteInputStreamCommand.COMMAND_TYPE };
}
/**
* Sets the last access time to the given timestamp for the stream identified by the given ID.
*
* @param stream_id identifies the stream that has just been accessed
* @param timestamp the timestamp to be considered the new last access time for the stream
*/
private void setLastAccess(Long stream_id, long timestamp) {
AtomicLong lat;
synchronized (m_lock) {
lat = m_lastAccessTimes.get(stream_id);
}
// do not set the timestamp if it is 0 or less because that means the stream was closed or removed and should be reaped
if ((lat != null) && (lat.get() > 0L)) {
lat.set(timestamp);
}
return;
}
/**
* This method will create a <code>TimerTask</code> whose responsibility is to check when the identified stream has
* been idle for longer than the allowed max idle time and, when it is, to remove that stream from this service.
*
* @param stream_id the ID of the stream that this task will check
* @param lat the object containing the last time the stream was accessed
*/
private void createIdleTimerTask(final Long stream_id, final AtomicLong lat) {
TimerTask task = new TimerTask() {
@Override
public void run() {
if ((System.currentTimeMillis() - lat.get()) > m_maxIdleTime) {
try {
if (removeInputStream(stream_id)) {
LOG.debug(CommI18NResourceKeys.TIMER_TASK_REMOVED_IDLE_STREAM, stream_id, m_maxIdleTime);
}
} catch (Exception e) {
LOG.warn(CommI18NResourceKeys.TIMER_TASK_CANNOT_REMOVE_STREAM, stream_id, e);
}
// we don't need to run this task anymore - make sure we kill it
cancel();
return;
}
}
};
m_idleTimer.schedule(task, 5000L, 5000L);
return;
}
}