/******************************************************************************* * This file is part of OpenNMS(R). * * Copyright (C) 2006-2011 The OpenNMS Group, Inc. * OpenNMS(R) is Copyright (C) 1999-2011 The OpenNMS Group, Inc. * * OpenNMS(R) is a registered trademark of The OpenNMS Group, Inc. * * OpenNMS(R) 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 3 of the License, * or (at your option) any later version. * * OpenNMS(R) 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 OpenNMS(R). If not, see: * http://www.gnu.org/licenses/ * * For more information contact: * OpenNMS(R) Licensing <license@opennms.org> * http://www.opennms.org/ * http://www.opennms.com/ *******************************************************************************/ package org.opennms.netmgt.eventd.adaptors.tcp; import java.io.BufferedInputStream; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.io.StringWriter; import java.io.Writer; import java.net.InetAddress; import java.net.Socket; import java.text.DateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.Date; import java.util.LinkedList; import java.util.List; import org.apache.commons.io.IOUtils; import org.opennms.core.fiber.Fiber; import org.opennms.core.utils.InetAddressUtils; import org.opennms.core.utils.LogUtils; import org.opennms.core.xml.JaxbUtils; import org.opennms.netmgt.eventd.adaptors.EventHandler; import org.opennms.netmgt.xml.event.Event; import org.opennms.netmgt.xml.event.EventReceipt; import org.opennms.netmgt.xml.event.Log; import org.xml.sax.InputSource; /** * Provides the logic and context of execution necessary to actually process a * client's event XML document. When a new stream handler is created and * assigned to an execution context it will unmarshal the remote document. The * events from the remote document are then passed to the registered event * handlers. All successfully processed events are acknowledged to the client by * the generation of an XML event receipt. * * @author <a href="mailto:weave@oculan.com">Brian Weaver </a> * @author <a href="http;//www.opennms.org">OpenNMS </a> * */ final class TcpStreamHandler implements Runnable { /** * The registered list of event handlers. Each incoming event will be * passed to all event handlers. The event handlers <em>MUST NOT</em> * modify the passed event. */ private List<EventHandler> m_handlers; /** * Set to stop the thread context. */ private volatile boolean m_stop; /** * The parent of this stream handler. */ private Fiber m_parent; /** * The socket connection to receive and process events from. The * successfully processed events will be acknowledged in an event-recipt * document. */ private Socket m_connection; /** * The thread context this runnable is executing in */ private Thread m_context; /** * The number of records per connection */ private int m_recsPerConn; /** * Constructs a new TCP/IP stream handler to process the remote document. * * @param parent * The parent fiber * @param sock * The socket connection * @param handlers * The list of event handlers. * @param number * The number of event records to process */ TcpStreamHandler(Fiber parent, Socket sock, List<EventHandler> handlers, int number) { m_parent = parent; m_connection = sock; m_handlers = handlers; m_stop = false; m_context = null; m_recsPerConn = number; } /** * Returns true if the context is alive. */ boolean isAlive() { boolean rc = false; if (m_context != null) rc = m_context.isAlive(); return rc; } /** * Stops and joins the context. */ void stop() throws InterruptedException { m_stop = true; if (m_context != null) { LogUtils.debugf(this, "Interrupting and joining the thread context %s", m_context.getName()); m_context.interrupt(); m_context.join(); LogUtils.debugf(this, "Context stopped and joined"); } } /** * The main execution context for processing a remote XML document. Once the * document is processed and an event receipt is returned to the client the * thread will exit. */ public void run() { // get the context and stop if necessary m_context = Thread.currentThread(); synchronized (m_context) { m_context.notifyAll(); } // check the stop flag if (m_stop) { LogUtils.debugf(this, "The stop flag was set prior to thread entry, closing connection"); try { m_connection.close(); } catch (final IOException e) { LogUtils.errorf(this, e, "An error occured while closing the connection."); } LogUtils.debugf(this, "Thread context exiting"); return; } // Log the startup of this stream handler final InetAddress sender = m_connection.getInetAddress(); LogUtils.debugf(this, "Event Log Stream Handler Started for %s", sender); /* * This linked list is used to exchange * instances of PipedOutputStreams. Whenever a * pipe output stream is recovered it must be * signaled to inform the EOT thread of the * ability to write to the pipe. Also, when * the descriptor is close a EOFException is * passed on the list. */ final LinkedList<Object> pipeXchange = new LinkedList<Object>(); final TcpRecordHandler chunker = new TcpRecordHandler(m_connection, pipeXchange); final Thread tchunker = new Thread(chunker, "TCPRecord Chunker[" + InetAddressUtils.str(m_connection.getInetAddress()) + ":" + m_connection.getPort() + "]"); synchronized (tchunker) { tchunker.start(); try { tchunker.wait(); } catch (final InterruptedException e) { LogUtils.errorf(this, e, "The thread was interrupted."); } } MAINLOOP: while (!m_stop && m_parent.getStatus() != Fiber.STOP_PENDING && m_parent.getStatus() != Fiber.STOPPED && m_recsPerConn != 0) { // get a new pipe input stream PipedInputStream pipeIn = null; synchronized (pipeXchange) { while (pipeXchange.isEmpty()) { if (chunker.isAlive()) { try { pipeXchange.wait(500); } catch (final InterruptedException e) { LogUtils.errorf(this, e, "The thread was interrupted."); break MAINLOOP; } } else { break MAINLOOP; } } // if an exception occured then just exit the BAL (Big Ass Loop) final Object o = pipeXchange.removeFirst(); if (o instanceof Throwable) { break MAINLOOP; } // construct the other end of the pipe try { pipeIn = new PipedInputStream((PipedOutputStream) o); } catch (final IOException e) { LogUtils.errorf(this, e, "An I/O exception occured construction a record reader."); break MAINLOOP; } // signal that we got the stream synchronized (o) { o.notify(); } } // decrement the record count if greater than zero m_recsPerConn -= (m_recsPerConn > 0 ? 1 : 0); // convert the pipe input stream into a buffered input stream final InputStream stream = new BufferedInputStream(pipeIn); // Unmarshal the XML document Log eLog = null; boolean doCleanup = false; try { eLog = JaxbUtils.unmarshal(Log.class, new InputSource(stream)); LogUtils.debugf(this, "Event record converted"); } catch (final Exception e) { LogUtils.errorf(this, e, "Could not unmarshall the XML record."); doCleanup = true; } finally { if (stream != null) { IOUtils.closeQuietly(stream); } } // clean up the data on the current pipe if necessary if (doCleanup) { /* * Cleanup a failed record. Need to read * the remaining bytes from the other thread * to synchronize up. The other thread might * be blocked writing. */ try { while (stream.read() != -1) { /* do nothing */; } } catch (final IOException e) { // do nothing } // start from the top! continue MAINLOOP; } // Now that we have a list of events, process them final Event[] events = eLog.getEvents().getEvent(); // sort the events by time Arrays.sort(events, new Comparator<Event>() { public int compare(final Event e1, final Event e2) { final boolean e1t = (e1.getTime() != null); final boolean e2t = (e2.getTime() != null); if (e1t && !e2t) { return 1; } else if (!e1t && e2t) { return -1; } else if (!e1t && !e2t) { return 0; } final DateFormat fmt = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL); Date de1 = null; try { de1 = fmt.parse(e1.getTime()); } catch (final Throwable t) { } Date de2 = null; try { de2 = fmt.parse(e2.getTime()); } catch (final Throwable t) { } if (de1 != null && de2 != null) { return (int) (de1.getTime() - de2.getTime()); } else if (de1 == null && de2 != null) { return -1; } else if (de1 != null && de2 == null) { return 1; } else { return 0; } } }); // process the events if (events != null && events.length != 0) { final List<Event> okEvents = new ArrayList<Event>(events.length); /* * This synchronization loop will hold onto the lock * for a while. If the handlers are going to change * often, which is shouldn't then might want to consider * duplicating the handlers into an array before processing * the events. * * Doing the synchronization in the outer loop prevents spending * lots of cycles doing synchronization when it should not * normally be necesary. */ synchronized (m_handlers) { for (final EventHandler hdl : m_handlers) { /* * get the handler and then have it process all * the events in the document before moving to the * next event handler. */ for (final Event event : events) { /* * Process the event and log any errors, * but don't die on these errors */ try { LogUtils.debugf(this, "handling event: %s", event); // shortcut and BOTH parts MUST execute! if (hdl.processEvent(event)) { if (!okEvents.contains(event)) { okEvents.add(event); } } } catch (final Throwable t) { LogUtils.warnf(this, t, "An exception occured while processing an event."); } } } } // Now process the good events and send a receipt message boolean hasReceipt = false; final EventReceipt receipt = new EventReceipt(); for (final Event event : okEvents) { if (event.getUuid() != null) { receipt.addUuid(event.getUuid()); hasReceipt = true; } } if (hasReceipt) { // Transform it to XML and send it to the socket in one call try { final Writer writer = new BufferedWriter(new OutputStreamWriter(m_connection.getOutputStream(), "UTF-8")); JaxbUtils.marshal(receipt, writer); writer.flush(); synchronized (m_handlers) { for (final EventHandler hdl : m_handlers) { /* * Get the handler and then have it process all * the events in the document before moving to * the next event hander. */ try { hdl.receiptSent(receipt); } catch (final Throwable t) { LogUtils.warnf(this, t, "An exception occured while processing an event receipt."); } } } if (LogUtils.isDebugEnabled(this)) { try { final StringWriter swriter = new StringWriter(); JaxbUtils.marshal(receipt, swriter); LogUtils.debugf(this, "Sent Event Receipt {"); LogUtils.debugf(this, swriter.getBuffer().toString()); LogUtils.debugf(this, "}"); } catch (final Throwable e) { LogUtils.errorf(this, e, "An error occured during marshalling of event receipt for the log."); } } } catch (final IOException e) { LogUtils.warnf(this, e, "Failed to send event-receipt XML document."); break MAINLOOP; } } } else { LogUtils.debugf(this, "The agent sent an empty event stream"); } } try { LogUtils.debugf(this, "stopping record handler"); chunker.stop(); LogUtils.debugf(this, "record handler stopped"); } catch (final InterruptedException e) { LogUtils.warnf(this, e, "The thread was interrupted while trying to close the record handler."); } // regardless of any errors, be sure to release the socket. try { LogUtils.debugf(this, "closing connnection"); m_connection.close(); LogUtils.debugf(this, "connnection closed "); } catch (final IOException e) { LogUtils.warnf(this, e, "An I/O exception occured while closing the TCP/IP connection."); } LogUtils.debugf(this, "Thread exiting"); } }