/**********************************************************************
* $Source: /cvsroot/jameica/jameica.webadmin/src/de/willuhn/jameica/webadmin/server/RestServiceImpl.java,v $
* $Revision: 1.29 $
* $Date: 2011/06/28 09:56:25 $
* $Author: willuhn $
* $Locker: $
* $State: Exp $
*
* Copyright (c) by willuhn software & services
* All rights reserved
*
**********************************************************************/
package de.willuhn.jameica.webadmin.server;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.json.JSONArray;
import org.json.JSONObject;
import de.willuhn.annotation.Inject;
import de.willuhn.annotation.Lifecycle;
import de.willuhn.jameica.messaging.Message;
import de.willuhn.jameica.messaging.MessageConsumer;
import de.willuhn.jameica.messaging.QueryMessage;
import de.willuhn.jameica.system.Application;
import de.willuhn.jameica.webadmin.annotation.Doc;
import de.willuhn.jameica.webadmin.annotation.Path;
import de.willuhn.jameica.webadmin.annotation.Request;
import de.willuhn.jameica.webadmin.annotation.Response;
import de.willuhn.jameica.webadmin.beans.RestBeanDoc;
import de.willuhn.jameica.webadmin.beans.RestMethodDoc;
import de.willuhn.jameica.webadmin.rmi.RestService;
import de.willuhn.logging.Logger;
import de.willuhn.util.ApplicationException;
/**
* Implementierung des REST-Services.
*/
public class RestServiceImpl implements RestService
{
private Map<String,Method> commands = null;
private Map<String,Object> contextScope = null;
private List<RestBeanDoc> doc = null;
private MessageConsumer register = new RestConsumer(true);
private MessageConsumer unregister = new RestConsumer(false);
/**
* @see de.willuhn.jameica.webadmin.rmi.RestService#handleRequest(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
*/
public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws IOException
{
if (!this.isStarted())
throw new IOException("REST service not started");
// Wie Umlaute im Query-String (also URL+GET-Parameter) codiert sind,
// ist nirgends so richtig spezifiziert. Also schicken es die
// Browser unterschiedlich. Jetty interpretiert es als UTF-8.
// Wenn man auf dem Client jedoch ISO-8859-15 als Zeichensatz
// eingestellt hat, schickt es auch der Browser damit.
// Effekt: Der Server weiss nicht, wie er die Umlaute interpretieren soll.
// Das fuehrt bisweilen soweit, dass nicht nur die Umlaute in request.getParameter(name)
// kaputt sind sondern ggf. auch noch die Buchstaben hinter dem Umlaut
// verloren gehen.
// Immerhin bietet Jetty eine Funktion an, um das Encoding
// vorzugeben. Das gaenge mit explizit mit:
// ((org.mortbay.jetty.Request)request).setQueryEncoding(String);
// oder implizit via (damit muss der Request nicht auf org.mortbay.jetty.Request gecastet werden:
// request.setAttribute("org.mortbay.jetty.Request.queryEncoding","String");
// siehe hierzu auch
// http://jira.codehaus.org/browse/JETTY-113
// http://jetty.mortbay.org/jetty5/faq/faq_s_900-Content_t_International.html
String queryencoding = de.willuhn.jameica.webadmin.Settings.SETTINGS.getString("http.queryencoding",null);
int jsonIndent = de.willuhn.jameica.webadmin.Settings.SETTINGS.getInt("json.indent",2);
if (queryencoding != null)
{
Logger.debug("query encoding: " + queryencoding);
request.setAttribute("org.mortbay.jetty.Request.queryEncoding",queryencoding);
}
String command = request.getPathInfo();
if (command == null)
throw new IOException("missing REST command");
Iterator<String> patterns = this.commands.keySet().iterator();
try
{
while (patterns.hasNext())
{
String path = patterns.next();
Method method = this.commands.get(path);
Pattern pattern = Pattern.compile(path);
Matcher match = pattern.matcher(command);
if (match.matches())
{
Object[] params = new Object[match.groupCount()];
for (int k=0;k<params.length;++k)
params[k] = match.group(k+1); // wir fangen bei "1" an, weil an Pos. 0 der Pattern selbst steht
Object bean = getBean(method.getDeclaringClass(),request);
applyAnnotations(bean, request, response);
Logger.debug("applying command " + path + " to " + method);
Object value = method.invoke(bean,params);
if (method.getReturnType() != null && value != null)
{
String s = null;
if ((value instanceof JSONObject) && jsonIndent > 0)
s = ((JSONObject)value).toString(jsonIndent);
else if ((value instanceof JSONArray) && jsonIndent > 0)
s = ((JSONArray)value).toString(jsonIndent);
else
s = value.toString();
response.getWriter().print(s);
}
return;
}
}
}
catch (Exception e)
{
String errorText = null;
if (e instanceof InvocationTargetException)
{
Throwable cause = e.getCause();
if (cause != null && (cause instanceof IOException || cause instanceof ApplicationException))
errorText = cause.getMessage();
}
if (errorText == null)
{
// Wir wissen nicht, wie wir den Fehler behandeln sollen.
// Also ist es ein unerwarteter Fehler - und den loggen wir
Logger.error("error while executing command " + command,e);
errorText = e.getMessage();
}
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,errorText);
return;
}
throw new IOException("no command found for REST url " + command);
}
/**
* Liefert eine Instanz der Bean.
* Die Funktion wertet die Lifecycle-Annotation der Bean aus
* und verwendet eine eventuell vorhandene Instanz, falls sie
* mit dem Lifecycle Context oder Session markiert ist.
* @param c die Klasse der Bean.
* @param request brauchen wir, um in der Session nachschauen zu koennen.
* @return die Instanz der Bean.
* @throws Exception
*/
private Object getBean(Class c, HttpServletRequest request) throws Exception
{
String id = c.getName();
Object bean = null;
// 1. Checken, ob wir sie im Context-Scope haben
bean = contextScope.get(id);
if (bean != null)
return bean;
// 2. Checken, ob wir sie im Session-Scope haben
HttpSession session = request.getSession();
bean = session.getAttribute(id);
if (bean != null)
return bean;
// 3. Bean erzeugen
bean = c.newInstance();
// Ggg in Context oder Session speichern
Lifecycle lc = (Lifecycle) c.getAnnotation(Lifecycle.class);
Lifecycle.Type type = lc != null ? lc.value() : Lifecycle.Type.REQUEST;
switch(type)
{
case CONTEXT:
contextScope.put(id,bean);
break;
case SESSION:
session.setAttribute(id,bean);
break;
}
return bean;
}
/**
* @see de.willuhn.jameica.webadmin.rmi.RestService#register(java.lang.Object)
*/
public void register(Object bean) throws RemoteException
{
if (!isStarted())
throw new RemoteException("REST service not started");
Hashtable<String,Method> found = eval(bean);
if (found.size() > 0)
{
Logger.info("register REST commands for " + bean.getClass());
this.commands.putAll(found);
//////////////////////////////////////////////////////////////////////////
// Dokumentation der REST-Bean
RestBeanDoc bd = new RestBeanDoc();
bd.setBeanClass(bean.getClass());
Doc d = bean.getClass().getAnnotation(Doc.class);
if (d != null)
bd.setText(d.value());
Enumeration<String> e = found.keys();
List<RestMethodDoc> methods = new ArrayList<RestMethodDoc>();
while (e.hasMoreElements())
{
String path = e.nextElement();
Method m = found.get(path);
RestMethodDoc md = new RestMethodDoc();
md.setPath(path);
md.setMethod(m.getName());
d = m.getAnnotation(Doc.class);
if (d != null)
{
md.setText(d.value());
md.setExample(d.example());
}
methods.add(md);
}
bd.setMethods(methods);
this.doc.add(bd);
//
//////////////////////////////////////////////////////////////////////////
}
else
{
Logger.warn(bean.getClass() + " contains no valid annotated methods, skip bean");
}
}
/**
* @see de.willuhn.jameica.webadmin.rmi.RestService#unregister(java.lang.Object)
*/
public void unregister(Object bean) throws RemoteException
{
if (!isStarted())
{
Logger.info("REST service not started");
return;
}
Hashtable<String,Method> found = eval(bean);
if (found.size() > 0)
{
Logger.info("un-register REST commands for " + bean.getClass());
Iterator<String> s = found.keySet().iterator();
while (s.hasNext())
{
if (!this.isStarted())
{
Logger.info("REST service not started");
return;
}
this.commands.remove(s.next());
}
for (RestBeanDoc bd:this.doc)
{
if (bd.getBeanClass().equals(bean.getClass()))
{
this.doc.remove(bd);
break;
}
}
}
else
{
Logger.warn(bean.getClass() + " contains no valid annotated methods, skip bean");
}
}
/**
* Analysiert die Bean und deren Annotations und liefert sie zurueck.
* @param bean die zu evaluierende Bean.
* @return Hashtable mit den URLs und zugehoerigen Methoden.
* @throws RemoteException
*/
private Hashtable<String,Method> eval(Object bean) throws RemoteException
{
if (bean == null)
throw new RemoteException("no REST bean given");
Hashtable<String,Method> found = new Hashtable<String,Method>();
Method[] methods = bean.getClass().getMethods();
for (Method m:methods)
{
Path path = m.getAnnotation(Path.class);
if (path == null)
continue;
String s = path.value();
if (s == null || s.length() == 0)
{
Logger.warn("no path specified for method " + m + ", skipping");
continue;
}
m.setAccessible(true);
Logger.debug("REST command " + m + ", URL pattern: " + s);
found.put(s,m);
}
return found;
}
/**
* Injiziert die Annotations.
* @param bean die Bean.
* @throws Exception
*/
private void applyAnnotations(Object bean, HttpServletRequest request, HttpServletResponse response) throws Exception
{
try
{
Inject.inject(bean,Request.class,request);
Inject.inject(bean,Response.class,response);
}
catch (Exception e)
{
Logger.error("unable to inject context",e);
throw new IOException("unable to inject context");
}
}
/**
* @see de.willuhn.datasource.Service#getName()
*/
public String getName() throws RemoteException
{
return "REST-API Service";
}
/**
* @see de.willuhn.datasource.Service#isStartable()
*/
public boolean isStartable() throws RemoteException
{
return !this.isStarted();
}
/**
* @see de.willuhn.datasource.Service#isStarted()
*/
public boolean isStarted() throws RemoteException
{
return this.commands != null;
}
/**
* @see de.willuhn.datasource.Service#start()
*/
public void start() throws RemoteException
{
if (isStarted())
{
Logger.warn("service allread started, skipping request");
return;
}
Logger.info("init REST registry");
this.commands = new Hashtable<String,Method>();
this.contextScope = new Hashtable<String,Object>();
this.doc = new ArrayList<RestBeanDoc>();
Application.getMessagingFactory().getMessagingQueue("jameica.webadmin.rest.register").registerMessageConsumer(this.register);
Application.getMessagingFactory().getMessagingQueue("jameica.webadmin.rest.unregister").registerMessageConsumer(this.unregister);
// Fremdsysteme benachrichtigen, dass wir online sind.
Application.getMessagingFactory().getMessagingQueue("jameica.webadmin.rest.start").sendMessage(new QueryMessage());
}
/**
* @see de.willuhn.datasource.Service#stop(boolean)
*/
public void stop(boolean arg0) throws RemoteException
{
if (!this.isStarted())
{
Logger.warn("service not started, skipping request");
return;
}
try
{
Application.getMessagingFactory().getMessagingQueue("jameica.webadmin.rest.register").unRegisterMessageConsumer(this.register);
Application.getMessagingFactory().getMessagingQueue("jameica.webadmin.rest.unregister").unRegisterMessageConsumer(this.unregister);
}
finally
{
Logger.info("REST service stopped");
this.contextScope = null;
this.commands = null;
this.doc = null;
}
}
/**
* @see de.willuhn.jameica.webadmin.rmi.RestService#getDoc()
*/
public List<RestBeanDoc> getDoc() throws RemoteException
{
return this.doc;
}
/**
* Hilfsklasse zum Registrieren von REST-Kommandos via Messaging
*/
private class RestConsumer implements MessageConsumer
{
private boolean r = false;
/**
* @param register true zum Registrieren, false zum De-Registrieren.
*/
private RestConsumer(boolean register)
{
this.r = register;
}
/**
* @see de.willuhn.jameica.messaging.MessageConsumer#autoRegister()
*/
public boolean autoRegister()
{
return false;
}
/**
* @see de.willuhn.jameica.messaging.MessageConsumer#getExpectedMessageTypes()
*/
public Class[] getExpectedMessageTypes()
{
return new Class[]{QueryMessage.class};
}
/**
* @see de.willuhn.jameica.messaging.MessageConsumer#handleMessage(de.willuhn.jameica.messaging.Message)
*/
public void handleMessage(Message message) throws Exception
{
QueryMessage m = (QueryMessage) message;
Object bean = m.getData();
if (bean == null)
return;
if (r)
register(bean);
else
unregister(bean);
}
}
}
/*********************************************************************
* $Log: RestServiceImpl.java,v $
* Revision 1.29 2011/06/28 09:56:25 willuhn
* @N Lifecycle-Annotation aus jameica.webadmin in util verschoben
*
* Revision 1.28 2011-03-30 12:14:05 willuhn
* @N Neuer Injector fuer DI
*
* Revision 1.27 2010/05/18 10:43:20 willuhn
* @N Lesbarere Fehlermeldungen
*
* Revision 1.26 2010/05/11 23:21:44 willuhn
* @N Automatische Dokumentations-Seite fuer die REST-Beans basierend auf der Annotation "Doc"
*
* Revision 1.25 2010/05/11 16:41:20 willuhn
* @N Automatisches Indent bei JSON-Objekten
*
* Revision 1.24 2010/05/11 14:59:48 willuhn
* @N Automatisches Deployment von REST-Beans
*
* Revision 1.23 2010/03/31 16:01:09 willuhn
* @B Compile-Fehler unter JDK 1.5
*
* Revision 1.22 2010/03/19 16:04:13 willuhn
* @N Exception durchreichen
*
* Revision 1.21 2010/03/19 15:56:17 willuhn
* @N LifeCycle-Annotation-Support jetzt auch fuer REST-Beans
*
* Revision 1.20 2010/03/18 09:29:35 willuhn
* @N Wenn REST-Beans Rueckgabe-Werte liefern, werrden sie automatisch als toString() in den Response-Writer geschrieben
*
* Revision 1.19 2010/02/10 13:43:48 willuhn
* @N InvocationTargetException entpacken
*
* Revision 1.18 2009/12/08 16:46:14 willuhn
* @B NPE
*
* Revision 1.17 2009/09/10 16:48:39 willuhn
* @C Annotations via BeanUtils ermitteln
*
* Revision 1.16 2009/08/05 09:03:40 willuhn
* @C Annotations in eigenes Package verschoben (sind nicht mehr REST-spezifisch)
*
* Revision 1.15 2009/01/06 01:44:14 willuhn
* @N Code zum Hinzufuegen von Servern erweitert
*
* Revision 1.14 2008/11/07 00:14:37 willuhn
* @N REST-Bean fuer Anzeige von System-Infos (Start-Zeit, Config)
*
* Revision 1.13 2008/11/06 23:29:15 willuhn
* @B s/return/continue/
*
* Revision 1.12 2008/10/21 22:33:47 willuhn
* @N Markieren der zu registrierenden REST-Kommandos via Annotation
*
* Revision 1.11 2008/10/08 21:38:23 willuhn
* @C Nur noch zwei Annotations "Request" und "Response"
*
* Revision 1.10 2008/10/08 17:54:32 willuhn
* @B message an der falschen Stelle geschickt
*
* Revision 1.9 2008/10/08 16:01:38 willuhn
* @N REST-Services via Injection (mittels Annotation) mit Context-Daten befuellen
*
* Revision 1.8 2008/10/07 23:45:16 willuhn
* @N Registrieren/Deregistrieren von REST-Commands via Messaging
*
* Revision 1.7 2008/09/09 14:40:09 willuhn
* @D Hinweise zum Encoding. Siehe auch http://www.willuhn.de/blog/index.php?/archives/415-Umlaute-in-URLs-sind-Mist.html
*
* Revision 1.6 2008/07/11 15:38:55 willuhn
* @N Service-Deployment
*
* Revision 1.5 2008/06/16 22:31:53 willuhn
* @N weitere REST-Kommandos
*
* Revision 1.4 2008/06/16 14:22:11 willuhn
* @N Mapping der REST-URLs via Property-Datei
*
* Revision 1.3 2008/06/15 22:48:24 willuhn
* @N Command-Chains
*
* Revision 1.2 2008/06/13 15:11:01 willuhn
* *** empty log message ***
*
* Revision 1.1 2008/06/13 14:11:04 willuhn
* @N Mini REST-API
*
**********************************************************************/