/*******************************************************************************
* 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.rtc;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import org.apache.commons.io.IOUtils;
import org.opennms.core.concurrent.LogPreservingThreadFactory;
import org.opennms.core.fiber.Fiber;
import org.opennms.core.utils.HttpUtils;
import org.opennms.core.utils.ThreadCategory;
import org.opennms.netmgt.config.RTCConfigFactory;
import org.opennms.netmgt.rtc.datablock.HttpPostInfo;
import org.opennms.netmgt.rtc.datablock.RTCCategory;
import org.opennms.netmgt.rtc.utils.EuiLevelMapper;
import org.opennms.netmgt.rtc.utils.PipedMarshaller;
import org.opennms.netmgt.xml.rtc.EuiLevel;
/**
* The DataSender is responsible to send data out to 'listeners'
*
* When the RTCManager's timers go off, the DataSender is prompted to send data,
* which it does by maintaining a 'SendRequest' runnable queue so as to not
* block the RTCManager
*
* @author <A HREF="mailto:sowmya@opennms.org">Sowmya Nataraj</A>
* @author <A HREF="mailto:weave@oculan.com">Brian Weaver</A>
* @author <A HREF="http://www.opennms.org">OpenNMS.org</A>
*/
final class DataSender implements Fiber {
/**
* The category map
*/
private Map<String,RTCCategory> m_categories;
/**
* The listeners like the WebUI that send a URL to which the data is to be
* sent
*/
private Map<String, Set<HttpPostInfo>> m_catUrlMap;
/**
* The data sender thread pool
*/
private ExecutorService m_dsrPool;
/**
* The category to XML mapper
*/
private EuiLevelMapper m_euiMapper;
/**
* The allowable number of times posts can have errors before an URL is
* automatically unsubscribed
*/
private final int POST_ERROR_LIMIT;
/**
* The current status of this fiber
*/
private int m_status;
/**
* Inner class to send data to all the categories - this runnable prevents
* the RTCManager from having to block until data is computed, converted to
* XML and sent out
*/
private class SendRequest implements Runnable {
/**
* Call the 'sendData()' to send the data out for all the categories
*/
public void run() {
sendData();
}
}
/**
* Set the current thread's priority to the passed value and return the old
* priority
*/
private int setCurrentThreadPriority(int priority) {
Thread currentThread = Thread.currentThread();
int oldPriority = currentThread.getPriority();
try {
currentThread.setPriority(priority);
} catch (Throwable e) {
if (log().isDebugEnabled()) {
log().debug("Error setting thread priority: ", e);
}
}
return oldPriority;
}
/**
* The constructor for this object
*
* @param categories
* The category map.
* @param numSenders
* The number of senders.
*/
public DataSender(Map<String, RTCCategory> categories, int numSenders) {
m_categories = categories;
// create the category URL map
m_catUrlMap = new HashMap<String, Set<HttpPostInfo>>();
// create and start the data sender pool
m_dsrPool = Executors.newFixedThreadPool(
numSenders,
new LogPreservingThreadFactory(getClass().getSimpleName(), numSenders, false)
);
// create category converter
m_euiMapper = new EuiLevelMapper();
// get post error limit
POST_ERROR_LIMIT = RTCConfigFactory.getInstance().getErrorsBeforeUrlUnsubscribe();
}
/**
* Start the data sender thread pool
*/
public synchronized void start() {
m_status = STARTING;
m_status = RUNNING;
}
private ThreadCategory log() {
return ThreadCategory.getInstance(this.getClass());
}
/**
* <P>
* Shutdown the data sender thread pool
*/
public synchronized void stop() {
m_status = STOP_PENDING;
log().info("DataSender - shutting down the data sender pool");
try {
m_dsrPool.shutdown();
} catch (Throwable e) {
log().error("Error shutting down data sender pool", e);
}
m_status = STOPPED;
log().info("DataSender shutdown complete");
}
/**
* Returns a name/ID for this fiber
*
* @return a {@link java.lang.String} object.
*/
public String getName() {
return "OpenNMS.RTC.DataSender";
}
/**
* Returns the current status
*
* @return a int.
*/
public int getStatus() {
return m_status;
}
/**
* Subscribe - Add the received URL and related info to the category->URLs map
* so the sendData() can send out to appropriate URLs for each category.
* Also send the latest info for the category
*
* @param url a {@link java.lang.String} object.
* @param catlabel a {@link java.lang.String} object.
* @param user a {@link java.lang.String} object.
* @param passwd a {@link java.lang.String} object.
*/
public synchronized void subscribe(String url, String catlabel, String user, String passwd) {
// send category data to the newly subscribed URL
// look up info for this category
RTCCategory cat = m_categories.get(catlabel);
if (cat == null) {
// oops! category for which we have no info!
log().warn("RTC: No information available for category: " + catlabel);
return;
}
// create new HttpPostInfo
HttpPostInfo postInfo = null;
try {
postInfo = new HttpPostInfo(url, catlabel, user, passwd);
} catch (MalformedURLException mue) {
log().warn("ERROR subscribing: Invalid URL \'" + url + "\' - Data WILL NOT be SENT to the specified url");
return;
}
// Add the URL to the list for the specified category
Set<HttpPostInfo> urlList = m_catUrlMap.get(catlabel);
if (urlList == null) {
urlList = new HashSet<HttpPostInfo>();
m_catUrlMap.put(catlabel, urlList);
}
if (!urlList.add(postInfo) && log().isDebugEnabled()) {
log().debug("Already subscribed to URL: " + url + "\tcatlabel: " + catlabel + "\tuser:" + user + " - IGNORING LATEST subscribe event");
} else {
if (log().isDebugEnabled()) {
log().debug("Subscribed to URL: " + url + "\tcatlabel: " + catlabel + "\tuser:" + user);
}
}
// send data
Reader inr = null;
InputStream inp = null;
try {
// Run at a higher than normal priority since we do have to send
// the update on time
int oldPriority = setCurrentThreadPriority(Thread.MAX_PRIORITY);
EuiLevel euidata = m_euiMapper.convertToEuiLevelXML(cat);
inr = new PipedMarshaller(euidata).getReader();
if (log().isDebugEnabled())
log().debug("DataSender: posting data to: " + url);
inp = HttpUtils.post(postInfo.getURL(), inr, user, passwd, 8 * HttpUtils.DEFAULT_POST_BUFFER_SIZE);
byte[] tmp = new byte[1024];
int bytesRead;
while ((bytesRead = inp.read(tmp)) != -1) {
if (log().isDebugEnabled()) {
if (bytesRead > 0)
log().debug("DataSender: post response: " + new String(tmp, 0, bytesRead));
}
}
// return current thread to its previous priority
oldPriority = setCurrentThreadPriority(oldPriority);
if (log().isDebugEnabled())
log().debug("DataSender: posted data for category: " + catlabel);
} catch (IOException ioE) {
log().warn("DataSender: Unable to send category \'" + catlabel + "\' to URL \'" + url + "\': ", ioE);
setCurrentThreadPriority(Thread.NORM_PRIORITY);
} catch (java.lang.OutOfMemoryError oe) {
log().warn("DataSender: Unable to send category \'" + catlabel + "\' to URL \'" + url + "\': ", oe);
setCurrentThreadPriority(Thread.NORM_PRIORITY);
} catch (RuntimeException e) {
log().warn("DataSender: Unable to send category \'" + catlabel + "\' to URL \'" + url + "\': ", e);
setCurrentThreadPriority(Thread.NORM_PRIORITY);
} catch (Throwable t) {
log().warn("DataSender: Unable to send category \'" + catlabel + "\' to URL \'" + url + "\': ", t);
setCurrentThreadPriority(Thread.NORM_PRIORITY);
} finally {
IOUtils.closeQuietly(inp);
IOUtils.closeQuietly(inr);
}
}
/**
* Unsubscribe - remove the received URL and related info from the
* category->URLs map so the sendData() will know when it sends data out
*
* @param urlStr a {@link java.lang.String} object.
*/
public synchronized void unsubscribe(final String urlStr) {
URL url = null;
try {
url = new URL(urlStr);
} catch (MalformedURLException mue) {
log().warn("ERROR unsubscribing: Invalid URL: " + url);
return;
}
// go through the hashtable entries and remove entries with
// the specified URL
Set<HttpPostInfo> value;
for (String key : m_catUrlMap.keySet()) {
value = m_catUrlMap.get(key);
if (value == null)
continue;
Iterator<HttpPostInfo> postSet = value.iterator();
while (postSet.hasNext()) {
HttpPostInfo postInfo = postSet.next();
if (url.equals(postInfo.getURL())) {
postSet.remove();
}
}
}
if (log().isDebugEnabled())
log().debug("Unsubscribed URL: " + url);
}
/**
* Loop through the categories and send out data for all categories that
* have changed
*/
public synchronized void sendData() {
log().debug("In DataSender sendData()");
// loop through and send info
for (RTCCategory cat : m_categories.values()) {
// get label
String catlabel = cat.getLabel();
if (log().isDebugEnabled())
log().debug("DataSender:sendData(): Category \'" + catlabel);
// get the post info for this category
Set<HttpPostInfo> urlList = m_catUrlMap.get(catlabel);
if (urlList == null || urlList.size() <= 0) {
// a category that no one is listening for?
if (log().isDebugEnabled())
log().debug("DataSender: category \'" + catlabel + "\' has no listeners");
continue;
}
if (log().isDebugEnabled())
log().debug("DataSender: category \'" + catlabel + "\' has listeners - converting to xml...");
// Run at a higher than normal priority since we do have to send
// the update on time
int oldPriority = setCurrentThreadPriority(Thread.MAX_PRIORITY);
EuiLevel euidata = null;
try {
euidata = m_euiMapper.convertToEuiLevelXML(cat);
} catch (java.lang.OutOfMemoryError oe) {
log().warn("DataSender: unable to convert data to xml for category: " + catlabel, oe);
setCurrentThreadPriority(Thread.NORM_PRIORITY);
continue;
} catch (Throwable t) {
log().warn("DataSender: unable to convert data to xml for category: " + catlabel, t);
setCurrentThreadPriority(Thread.NORM_PRIORITY);
}
// do a HTTP POST if subscribed
if (urlList != null && urlList.size() > 0) {
Iterator<HttpPostInfo> urlIter = urlList.iterator();
while (urlIter.hasNext()) {
HttpPostInfo postInfo = urlIter.next();
Reader inr = null;
InputStream inp = null;
try {
inr = new PipedMarshaller(euidata).getReader();
if (log().isDebugEnabled())
log().debug("DataSender: posting data to: " + postInfo.getURLString());
inp = HttpUtils.post(postInfo.getURL(), inr, postInfo.getUser(), postInfo.getPassword(), 8 * HttpUtils.DEFAULT_POST_BUFFER_SIZE);
if (log().isDebugEnabled())
log().debug("DataSender: posted data for category: " + catlabel);
byte[] tmp = new byte[1024];
int bytesRead;
while ((bytesRead = inp.read(tmp)) != -1) {
if (log().isDebugEnabled()) {
if (bytesRead > 0)
log().debug("DataSender: post response: " + new String(tmp, 0, bytesRead));
}
}
postInfo.clearErrors();
} catch (IOException e) {
log().warn("DataSender: unable to send data for category: " + catlabel + " due to " + e.getClass().getName() + ": " + e.getMessage(), e);
postInfo.incrementErrors();
setCurrentThreadPriority(Thread.NORM_PRIORITY);
} catch (java.lang.OutOfMemoryError e) {
log().warn("DataSender: unable to send data for category: " + catlabel + " due to " + e.getClass().getName() + ": " + e.getMessage(), e);
setCurrentThreadPriority(Thread.NORM_PRIORITY);
} catch (RuntimeException e) {
log().warn("DataSender: unable to send data for category: " + catlabel + " due to " + e.getClass().getName() + ": " + e.getMessage(), e);
setCurrentThreadPriority(Thread.NORM_PRIORITY);
} catch (Throwable t) {
log().warn("DataSender: unable to send data for category: " + catlabel + " due to " + t.getClass().getName() + ": " + t.getMessage(), t);
setCurrentThreadPriority(Thread.NORM_PRIORITY);
} finally {
IOUtils.closeQuietly(inp);
IOUtils.closeQuietly(inr);
}
// check to see if URL had too many errors
if (POST_ERROR_LIMIT > 0 && postInfo.getErrors() >= POST_ERROR_LIMIT) {
// unsubscribe the URL
urlIter.remove();
log().warn("URL " + postInfo.getURLString() + " UNSUBSCRIBED due to reaching error limit " + postInfo.getErrors());
}
}
}
// return current thread to its previous priority
oldPriority = setCurrentThreadPriority(oldPriority);
}
}
/**
* Notify the DataSender to send data - create a new 'SendRequest' to send
* the data and queue to the consumer
*/
public synchronized void notifyToSend() {
try {
m_dsrPool.execute(new SendRequest());
} catch (RejectedExecutionException e) {
log().warn("Unable to queue datasender to the dsConsumer queue", e);
}
}
}