/*******************************************************************************
* This file is part of OpenNMS(R).
*
* Copyright (C) 2010-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.rt;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.params.ClientPNames;
import org.apache.http.client.params.CookiePolicy;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.HttpParams;
import org.apache.http.util.EntityUtils;
import org.opennms.core.utils.LogUtils;
public class RequestTracker {
private final String m_baseURL;
private String m_user;
private String m_password;
private int m_timeout;
private int m_retries;
private Pattern m_inTokensPattern = Pattern.compile("^(\\w+):\\s*(.*?)\\s*$", Pattern.MULTILINE);
private Pattern m_ticketCreatedPattern = Pattern.compile("(?s) Ticket (\\d+) created");
private Pattern m_ticketUpdatedPattern = Pattern.compile("(?s) Ticket (\\d+) updated");
private Pattern m_customFieldPatternOld = Pattern.compile("^C(?:ustom)?F(?:ield)?-(.*?):\\s*(.*?)\\s*$");
private Pattern m_customFieldPatternNew = Pattern.compile("^CF\\.\\{(.*?)\\}:\\s*(.*?)\\s*$");
private DefaultHttpClient m_client;
public RequestTracker(final String baseURL, final String username, final String password, int timeout, int retries) {
m_baseURL = baseURL;
m_user = username;
m_password = password;
m_timeout = timeout;
m_retries = retries;
}
public Long createTicket(final RTTicket ticket) throws RequestTrackerException {
final HttpPost post = new HttpPost(m_baseURL + "/REST/1.0/edit");
return postEdit(post, ticket.toContent(), m_ticketCreatedPattern);
}
public Long updateTicket(final Long id, final String content) throws RequestTrackerException {
HttpPost post = new HttpPost(m_baseURL + "/REST/1.0/ticket/" + id + "/edit");
return postEdit(post, content, m_ticketUpdatedPattern);
}
public Long postEdit(final HttpPost post, final String content, final Pattern pattern) throws RequestTrackerException {
String rtTicketNumber = null;
final List<NameValuePair> params = new ArrayList<NameValuePair>();
params.add(new BasicNameValuePair("user", m_user));
params.add(new BasicNameValuePair("pass", m_password));
params.add(new BasicNameValuePair("content", content));
try {
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(params, "UTF-8");
post.setEntity(entity);
} catch (final UnsupportedEncodingException e) {
// Should never happen
LogUtils.warnf(this, e, "unsupported encoding exception for UTF-8 -- WTF?!");
}
try {
final HttpResponse response = getClient().execute(post);
int responseCode = response.getStatusLine().getStatusCode();
if (responseCode != HttpStatus.SC_OK) {
throw new RequestTrackerException("Received a non-200 response code from the server: " + responseCode);
} else {
final String in = EntityUtils.toString(response.getEntity());
final Matcher matcher = pattern.matcher(in);
if (matcher.find()) {
rtTicketNumber = matcher.group(1);
} else {
LogUtils.debugf(this, "did not get ticket ID from response when posting to %s", post.toString());
}
}
} catch (final Exception e) {
LogUtils.errorf(this, e, "Failure attempting to update ticket.");
throw new RequestTrackerException(e);
}
if (rtTicketNumber == null) {
return null;
}
return Long.valueOf(rtTicketNumber);
}
public RTUser getUserInfo(final String username) {
getSession();
Map<String, String> attributes = Collections.emptyMap();
final HttpGet get = new HttpGet(m_baseURL + "/REST/1.0/user/" + username);
try {
final HttpResponse response = getClient().execute(get);
int responseCode = response.getStatusLine().getStatusCode();
if (responseCode != HttpStatus.SC_OK) {
throw new RequestTrackerException("Received a non-200 response code from the server: " + responseCode);
} else {
if (response.getEntity() != null) {
attributes = parseResponseStream(response.getEntity().getContent());
}
}
} catch (final Exception e) {
LogUtils.errorf(this, e, "An exception occurred while getting user info for " + username);
return null;
}
final String id = attributes.get("id");
final String realname = attributes.get("realname");
final String email = attributes.get("emailaddress");
if (id == null || "".equals(id)) {
LogUtils.errorf(this, "Unable to retrieve ID from user info.");
return null;
}
return new RTUser(Long.parseLong(id.replace("user/", "")), username, realname, email);
}
public RTTicket getTicket(final Long ticketId, boolean getTextAttachment) throws RequestTrackerException {
getSession();
Map<String, String> attributes = getTicketAttributes(ticketId.toString());
RTTicket ticket = new RTTicket();
if (attributes == null)
throw new RequestTrackerException("received no ticket attributes back from RT");
final String id = attributes.remove("id").replace("ticket/", "");
if (id != null && id.length() > 0) {
ticket.setId(Long.valueOf(id));
}
ticket.setQueue(attributes.remove("queue"));
ticket.setCreated(attributes.remove("created"));
ticket.setSubject(attributes.remove("subject"));
ticket.setText(attributes.remove("text"));
ticket.setStatus(attributes.remove("status"));
if (attributes.containsKey("requestors")) {
for (final String requestor : attributes.remove("requestors").split("\\s*,\\s*")) {
ticket.addRequestor(requestor);
}
} else if (attributes.containsKey("requestor")) {
ticket.setRequestor(attributes.remove("requestor"));
}
// We previously normalized to the new custom-field syntax, so no need to check here for the old
for (String bute : attributes.keySet()) {
String headerForm = bute + ": " + attributes.get(bute);
Matcher cfMatcher = m_customFieldPatternNew.matcher(headerForm);
if (cfMatcher.matches()) {
CustomField cf = new CustomField(cfMatcher.group(1));
cf.addValue(new CustomFieldValue(cfMatcher.group(2)));
attributes.remove(bute);
}
}
if (LogUtils.isTraceEnabled(this)) {
if (attributes.size() > 0) {
LogUtils.tracef(this, "unhandled RT ticket attributes: %s", attributes.keySet().toString());
}
}
if (ticket.getText() == null || ticket.getText().equals("") && getTextAttachment) {
attributes = getTicketAttributes(ticketId + "/attachments");
if (attributes.containsKey("attachments")) {
final Matcher matcher = m_inTokensPattern.matcher(attributes.get("attachments"));
matcher.find();
final String attachmentId = matcher.group(1);
if (attachmentId != null && !"".equals(attachmentId)) {
attributes = getTicketAttributes(ticketId + "/attachments/" + attachmentId);
if (attributes.containsKey("content")) {
ticket.setText(attributes.remove("content"));
}
}
LogUtils.debugf(this, "attachment ID = %s", attachmentId);
}
}
return ticket;
}
public List<RTTicket> getTicketsForQueue(final String queueName, long limit) {
getSession();
final List<NameValuePair> params = new ArrayList<NameValuePair>();
params.add(new BasicNameValuePair("query", "Queue='" + queueName + "' AND Status='open'"));
params.add(new BasicNameValuePair("format", "i"));
params.add(new BasicNameValuePair("orderby", "-id"));
final HttpGet get = new HttpGet(m_baseURL + "/REST/1.0/search/ticket?" + URLEncodedUtils.format(params, "UTF-8"));
final List<RTTicket> tickets = new ArrayList<RTTicket>();
final List<Long> ticketIds = new ArrayList<Long>();
try {
final HttpResponse response = getClient().execute(get);
int responseCode = response.getStatusLine().getStatusCode();
if (responseCode != HttpStatus.SC_OK) {
throw new RequestTrackerException("Received a non-200 response code from the server: " + responseCode);
} else {
InputStreamReader isr = null;
BufferedReader br = null;
try {
if (response.getEntity() == null) return null;
isr = new InputStreamReader(response.getEntity().getContent());
br = new BufferedReader(isr);
String line = null;
do {
line = br.readLine();
if (line != null ) {
if (line.contains("does not exist.")) {
return null;
}
if (line.startsWith("ticket/")) {
ticketIds.add(Long.parseLong(line.replace("ticket/", "")));
}
}
} while (line != null);
} catch (final Exception e) {
throw new RequestTrackerException("Unable to read ticket IDs from query.", e);
} finally {
IOUtils.closeQuietly(br);
IOUtils.closeQuietly(isr);
}
}
} catch (final Exception e) {
LogUtils.errorf(this, e, "An exception occurred while getting tickets for queue " + queueName);
return null;
}
for (final Long id : ticketIds) {
try {
tickets.add(getTicket(id, false));
} catch (final RequestTrackerException e) {
LogUtils.warnf(this, e, "Unable to retrieve ticket.");
}
}
return tickets;
}
public RTQueue getFirstPublicQueueForUser(final String username) throws RequestTrackerException {
if (username == null) {
LogUtils.errorf(this, "User name cannot be null.");
throw new RequestTrackerException("User name cannot be null.");
}
for (final RTQueue queue : getQueuesForUser(username)) {
if (queue.isAccessible() && !queue.getName().startsWith("___")) return queue;
}
return null;
}
public List<RTQueue> getQueuesForUser(final String username) throws RequestTrackerException {
if (username == null) {
LogUtils.errorf(this, "User name cannot be null.");
throw new RequestTrackerException("User name cannot be null.");
}
getSession();
final List<RTQueue> queues = new ArrayList<RTQueue>();
long id = 1;
RTQueue queue = null;
while (true) {
queue = getQueue(id);
if (queue == null) {
break;
}
if (queue.isAccessible() && queue.getName().startsWith("___")) {
LogUtils.debugf(this, "found queue: %s (skipping)", queue);
} else {
LogUtils.debugf(this, "found queue: %s", queue);
queues.add(queue);
}
id++;
}
return queues;
}
public RTQueue getQueue(long id) throws RequestTrackerException {
getSession();
Map<String, String> attributes = Collections.emptyMap();
final HttpGet get = new HttpGet(m_baseURL + "/REST/1.0/queue/" + id);
try {
final HttpResponse response = getClient().execute(get);
int responseCode = response.getStatusLine().getStatusCode();
if (responseCode != HttpStatus.SC_OK) {
throw new RequestTrackerException("Received a non-200 response code from the server: " + responseCode);
} else {
if (response.getEntity() == null) {
LogUtils.debugf(this, "no entity returned by HTTP client");
}
attributes = parseResponseStream(response.getEntity().getContent());
}
} catch (final Exception e) {
LogUtils.errorf(this, e, "An exception occurred while getting queue #" + id);
return null;
}
if (attributes.containsKey("id") && attributes.containsKey("name")) {
final String queueId = attributes.get("id").replace("queue/", "");
final long longId = Long.parseLong(queueId);
final String name = attributes.get("name").trim();
final String priority = attributes.get("finalpriority").trim();
LogUtils.debugf(this, "name = %s, priority = %s", name, priority);
if ("".equals(name) && "".equals(priority)) {
LogUtils.debugf(this, "We got a response back, but it had no name or priority; assuming we have no access to this queue.");
return new RTInaccessibleQueue(longId);
}
return new RTQueue(longId, attributes.get("name"));
} else {
LogUtils.debugf(this, "id or name missing (%d, %s)", attributes.get("id"), attributes.get("name"));
return null;
}
}
private Map<String, String> getTicketAttributes(final String ticketQuery) throws RequestTrackerException {
// don't try to get ticket if it's marked as not available
if (ticketQuery == null) {
LogUtils.errorf(this, "No ticket query specified!");
throw new RequestTrackerException("No ticket query specified!");
}
getSession();
Map<String,String> ticketAttributes = Collections.emptyMap();
final HttpGet get = new HttpGet(m_baseURL + "/REST/1.0/ticket/" + ticketQuery);
try {
final HttpResponse response = getClient().execute(get);
int responseCode = response.getStatusLine().getStatusCode();
if (responseCode != HttpStatus.SC_OK) {
throw new RequestTrackerException("Received a non-200 response code from the server: " + responseCode);
} else {
if (response.getEntity() == null) {
LogUtils.debugf(this, "no entity returned by HTTP client");
}
ticketAttributes = parseResponseStream(response.getEntity().getContent());
}
} catch (final Exception e) {
LogUtils.errorf(this, e, "HTTP exception attempting to get ticket.");
}
if (ticketAttributes.size() == 0) {
LogUtils.debugf(this, "matcher did not match %s", m_inTokensPattern.pattern());
return null;
}
return ticketAttributes;
}
protected Map<String,String> parseResponseStream(final InputStream responseStream) throws IOException {
final Map<String,String> ticketAttributes = new HashMap<String,String>();
LogUtils.debugf(this, "parsing response");
String lastIndent = "";
String lastKey = null;
for (final String line : (List<String>)IOUtils.readLines(responseStream)) {
LogUtils.tracef(this, "line = %s", line);
if (line.contains("does not exist.")) {
return ticketAttributes;
} if (lastIndent.length() > 0 && line.startsWith(lastIndent)) {
final String value = ticketAttributes.get(lastKey) + "\n" + line.replaceFirst("^" + lastIndent, "");
ticketAttributes.put(lastKey, value);
} else {
final Matcher inTokensMatcher = m_inTokensPattern.matcher(line);
final Matcher cfMatcherOld = m_customFieldPatternOld.matcher(line);
final Matcher cfMatcherNew = m_customFieldPatternNew.matcher(line);
if (inTokensMatcher.matches()) {
if (cfMatcherOld.matches()) {
lastKey = "CF.{" + cfMatcherOld.group(1) + "}";
} else if (cfMatcherNew.matches()) {
lastKey = "CF.{" + cfMatcherNew.group(1) + "}";
}
else {
lastKey = inTokensMatcher.group(1).toLowerCase();
}
lastIndent = lastKey.replaceAll(".", " ") + " ";
ticketAttributes.put(lastKey, inTokensMatcher.group(2));
}
}
}
return ticketAttributes;
}
private void getSession() {
if (m_client == null) {
// we need to log in at least once with a POST method before we can do any GETs so we get a session cookie
final HttpPost post = new HttpPost(m_baseURL + "/REST/1.0/user/" + m_user);
final List<NameValuePair> params = new ArrayList<NameValuePair>();
params.add(new BasicNameValuePair("user", m_user));
params.add(new BasicNameValuePair("pass", m_password));
try {
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(params, "UTF-8");
post.setEntity(entity);
} catch (final UnsupportedEncodingException e) {
// Should never happen
LogUtils.warnf(this, e, "unsupported encoding exception for UTF-8 -- WTF?!");
}
try {
final HttpResponse response = getClient().execute(post);
int responseCode = response.getStatusLine().getStatusCode();
if (responseCode != HttpStatus.SC_OK) {
throw new RequestTrackerException("Received a non-200 response code from the server: " + responseCode);
} else {
if (response.getEntity() != null) {
EntityUtils.consume(response.getEntity());
}
LogUtils.warnf(this, "got user session for username: %s", m_user);
}
} catch (final Exception e) {
LogUtils.warnf(this, e, "Unable to get session (by requesting user details)");
}
}
}
public synchronized HttpClient getClient() {
if (m_client == null) {
m_client = new DefaultHttpClient();
HttpParams clientParams = m_client.getParams();
clientParams.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, m_timeout);
clientParams.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, m_timeout);
clientParams.setParameter(ClientPNames.COOKIE_POLICY, CookiePolicy.BROWSER_COMPATIBILITY);
m_client.setParams(clientParams);
m_client.setHttpRequestRetryHandler(new DefaultHttpRequestRetryHandler(m_retries, false));
}
return m_client;
}
public synchronized void setClient(final DefaultHttpClient client) {
m_client = client;
}
public void setUser(final String user) {
m_user = user;
}
public void setPassword(final String password) {
m_password = password;
}
public String toString() {
return new ToStringBuilder(this)
.append("base-url", m_baseURL)
.append("username", m_user)
.append("password", m_password.replaceAll(".", "*"))
.append("timeout", m_timeout)
.append("retries", m_retries)
.toString();
}
public String getUsername() {
return m_user;
}
}