/*
*
* Copyright (C) 2012-2014 R T Huitema. All Rights Reserved.
* Web: www.42.co.nz
* Email: robert@42.co.nz
* Author: R T Huitema
*
* This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
* WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package nz.co.fortytwo.signalk.server;
import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import javax.servlet.http.HttpServletResponse;
import org.apache.camel.Exchange;
import org.apache.camel.ExchangePattern;
import org.apache.camel.Predicate;
import org.apache.camel.Processor;
import org.apache.camel.builder.PredicateBuilder;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.websocket.WebsocketConstants;
import org.apache.camel.component.websocket.WebsocketEndpoint;
import org.apache.camel.impl.DefaultCamelContext;
import org.apache.camel.model.RouteDefinition;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import mjson.Json;
import nz.co.fortytwo.signalk.processor.AISProcessor;
import nz.co.fortytwo.signalk.processor.AisExpiryProcessor;
import nz.co.fortytwo.signalk.processor.AlarmProcessor;
import nz.co.fortytwo.signalk.processor.AnchorWatchProcessor;
import nz.co.fortytwo.signalk.processor.ClientAppProcessor;
import nz.co.fortytwo.signalk.processor.ConfigFilterProcessor;
import nz.co.fortytwo.signalk.processor.DeclinationProcessor;
import nz.co.fortytwo.signalk.processor.DeltaImportProcessor;
import nz.co.fortytwo.signalk.processor.DepthProcessor;
import nz.co.fortytwo.signalk.processor.FullExportProcessor;
import nz.co.fortytwo.signalk.processor.FullImportProcessor;
import nz.co.fortytwo.signalk.processor.FullToDeltaProcessor;
import nz.co.fortytwo.signalk.processor.HeartbeatProcessor;
import nz.co.fortytwo.signalk.processor.IncomingSecurityFirewall;
import nz.co.fortytwo.signalk.processor.InputFilterProcessor;
import nz.co.fortytwo.signalk.processor.JsonGetProcessor;
import nz.co.fortytwo.signalk.processor.JsonListProcessor;
import nz.co.fortytwo.signalk.processor.JsonSubscribeProcessor;
import nz.co.fortytwo.signalk.processor.LoggerProcessor;
import nz.co.fortytwo.signalk.processor.MapToJsonProcessor;
import nz.co.fortytwo.signalk.processor.MqttProcessor;
import nz.co.fortytwo.signalk.processor.N2KProcessor;
import nz.co.fortytwo.signalk.processor.NMEA0183ExportProcessor;
import nz.co.fortytwo.signalk.processor.NMEAProcessor;
import nz.co.fortytwo.signalk.processor.OutputFilterProcessor;
import nz.co.fortytwo.signalk.processor.PermissionsProcessor;
import nz.co.fortytwo.signalk.processor.RestApiProcessor;
import nz.co.fortytwo.signalk.processor.RestAuthProcessor;
import nz.co.fortytwo.signalk.processor.RestPathFilterProcessor;
import nz.co.fortytwo.signalk.processor.SaveProcessor;
import nz.co.fortytwo.signalk.processor.SignalkModelProcessor;
import nz.co.fortytwo.signalk.processor.SourceRefToSourceProcessor;
import nz.co.fortytwo.signalk.processor.SourceToSourceRefProcessor;
import nz.co.fortytwo.signalk.processor.StompProcessor;
import nz.co.fortytwo.signalk.processor.TrackProcessor;
import nz.co.fortytwo.signalk.processor.UploadProcessor;
import nz.co.fortytwo.signalk.processor.ValidationProcessor;
import nz.co.fortytwo.signalk.processor.WindProcessor;
import nz.co.fortytwo.signalk.processor.WsSessionProcessor;
import nz.co.fortytwo.signalk.util.ConfigConstants;
import nz.co.fortytwo.signalk.util.SignalKConstants;
public class SignalkRouteFactory {
private static Logger logger = LogManager.getLogger(SignalkRouteFactory.class);
private static Set<String> nameSet = new HashSet<String>();
/**
* Configures a route for all input traffic, which will parse the traffic and update the signalk model
* @param routeBuilder
* @param input
* @param inputFilterProcessor
* @param nmeaProcessor
* @param aisProcessor
* @param signalkModelProcessor
* @throws IOException
* @throws Exception
*/
public static void configureInputRoute(RouteBuilder routeBuilder,String input) throws Exception {
routeBuilder.from(input).id(getName("INPUT"))
.onException(Exception.class).handled(true).maximumRedeliveries(0)
.to("log:nz.co.fortytwo.signalk.model.receive?level=ERROR&showException=true&showStackTrace=true")
.end()
// dump misc rubbish
.process(new InputFilterProcessor()).id(getName(InputFilterProcessor.class.getSimpleName()))
//now filter security
.process(new IncomingSecurityFirewall()).id(getName(IncomingSecurityFirewall.class.getSimpleName()))
//swap payloads to storage
//.process(new StorageProcessor()).id(getName(StorageProcessor.class.getSimpleName()))
//convert NMEA to signalk
.process(new NMEAProcessor()).id(getName(NMEAProcessor.class.getSimpleName()))
//convert AIS to signalk
.process(new AISProcessor()).id(getName(AISProcessor.class.getSimpleName()))
//convert n2k
.process(new N2KProcessor()).id(getName(N2KProcessor.class.getSimpleName()))
//handle subscribe messages
.process(new JsonSubscribeProcessor()).id(getName(JsonSubscribeProcessor.class.getSimpleName()))
//deal with delta format
.process(new DeltaImportProcessor()).id(getName(DeltaImportProcessor.class.getSimpleName()))
//deal with full format
.process(new FullImportProcessor()).id(getName(FullImportProcessor.class.getSimpleName()))
//make sure we have timestamp/source
.process(new ValidationProcessor()).id(getName(ValidationProcessor.class.getSimpleName()))
//record track
.process(new TrackProcessor()).id(getName(TrackProcessor.class.getSimpleName()))
//push source to sources and add $source
.process(new SourceToSourceRefProcessor()).id(getName(SourceToSourceRefProcessor.class.getSimpleName()))
//strip out according to meta owner,group,others
.process(new PermissionsProcessor()).id(getName(PermissionsProcessor.class.getSimpleName()))
//and update signalk model
.process(new SignalkModelProcessor()).id(getName(SignalkModelProcessor.class.getSimpleName()))
//we have processed all incoming data now - if there is more left its LIST, GET.
//handle list
.process(new JsonListProcessor()).id(getName(JsonListProcessor.class.getSimpleName()))
//handle get
.process(new JsonGetProcessor()).id(getName(JsonGetProcessor.class.getSimpleName()));
//.process(new StorageProcessor()).id(getName(StorageProcessor.class.getSimpleName()));
}
/**
* Configures the route for output to websockets
* @param routeBuilder
* @param input
*/
public static void configureWebsocketTxRoute(RouteBuilder routeBuilder ,String input, int port){
Predicate p1 = routeBuilder.header(ConfigConstants.OUTPUT_TYPE).isEqualTo(ConfigConstants.OUTPUT_WS);
Predicate p2 = routeBuilder.header(WebsocketConstants.CONNECTION_KEY).isEqualTo(WebsocketConstants.SEND_TO_ALL);
//from SEDA_WEBSOCKETS
routeBuilder.from(input).id(getName("Websocket Tx"))
.onException(Exception.class)
.handled(true)
.maximumRedeliveries(0)
.to("log:nz.co.fortytwo.signalk.model.websocket.tx?level=ERROR&showException=true&showStackTrace=true")
.end()
.filter(PredicateBuilder.or(p1, p2))
.to("skWebsocket://0.0.0.0:"+port+SignalKConstants.SIGNALK_WS).id(getName("Websocket Client"));
}
/**
* Configures the route for input to websockets
* @param routeBuilder
* @param input
* @throws Exception
*/
public static void configureWebsocketRxRoute(RouteBuilder routeBuilder ,String input, int port) {
WebsocketEndpoint wsEndpoint = (WebsocketEndpoint) routeBuilder.getContext().getEndpoint("skWebsocket://0.0.0.0:"+port+SignalKConstants.SIGNALK_WS);
wsEndpoint.setEnableJmx(true);
wsEndpoint.setSessionSupport(true);
wsEndpoint.setCrossOriginFilterOn(true);
wsEndpoint.setAllowedOrigins("*");
routeBuilder.from(wsEndpoint).id(getName("Websocket Rx"))
.onException(Exception.class)
.handled(true).maximumRedeliveries(0)
.to("log:nz.co.fortytwo.signalk.model.websocket.rx?level=ERROR&showException=true&showStackTrace=true")
.end()
.process(new WsSessionProcessor()).id(getName(WsSessionProcessor.class.getSimpleName()))
//.to("log:nz.co.fortytwo.signalk.model.websocket.rx?level=INFO&showException=true&showStackTrace=true")
.to(input).id(getName(RouteManager.SEDA_INPUT));
}
public static void configureTcpServerRoute(RouteBuilder routeBuilder ,String input, NettyServer nettyServer, String outputType) throws Exception{
// push out via TCPServer.
Predicate p1 = routeBuilder.header(ConfigConstants.OUTPUT_TYPE).isEqualTo(outputType);
Predicate p2 = routeBuilder.header(WebsocketConstants.CONNECTION_KEY).isEqualTo(WebsocketConstants.SEND_TO_ALL);
routeBuilder.from(input).id(getName("Netty "+outputType+" Server"))
.onException(Exception.class)
.handled(true)
.maximumRedeliveries(0)
.end()
.filter(PredicateBuilder.or(p1, p2))
.process((Processor) nettyServer).id(getName(NettyServer.class.getSimpleName())).end();
}
public static void configureRestRoute(RouteBuilder routeBuilder ,String input, String name)throws IOException{
routeBuilder.from(input).id(getName(name)) //.setExchangePattern(ExchangePattern.InOut);
.setExchangePattern(ExchangePattern.InOut)
.process(new RestApiProcessor())
.to(ExchangePattern.InOut,RouteManager.SEDA_INPUT)
.process(new ConfigFilterProcessor(false)).id(getName(ConfigFilterProcessor.class.getSimpleName()))
.process(new RestPathFilterProcessor()).id(getName(RestPathFilterProcessor.class.getSimpleName()));
// routeBuilder.rest(SignalKConstants.SIGNALK_API).id("REST POST Client")
// .post("/")
// .to("log:nz.co.fortytwo.signalk.client.rest?level=INFO&showException=true&showStackTrace=true")
// .to("direct:restTest");
}
public static void configureRestLoggerRoute(RouteBuilder routeBuilder ,String input, String name)throws IOException{
routeBuilder.from(input).id(getName(name))
.setExchangePattern(ExchangePattern.InOut)
.process(new LoggerProcessor()).id(getName(LoggerProcessor.class.getSimpleName()));
}
public static void configureRestUploadRoute(RouteBuilder routeBuilder ,String input, String name)throws IOException{
routeBuilder.rest(input).id(getName(name))
.post()
.route().id(getName(name+"Route"))
.setExchangePattern(ExchangePattern.InOut)
.process(new UploadProcessor()).id(getName(UploadProcessor.class.getSimpleName()));
}
public static void configureRestConfigRoute(RouteBuilder routeBuilder ,String input, String name)throws IOException{
routeBuilder.from(input).id(getName(name))
.setExchangePattern(ExchangePattern.InOut)
.process(new RestApiProcessor())
.to(ExchangePattern.InOut,RouteManager.SEDA_INPUT)
.process(new ConfigFilterProcessor(true)).id(getName(ConfigFilterProcessor.class.getSimpleName()));
}
public static void configureAuthRoute(RouteBuilder routeBuilder ,String input){
routeBuilder.from(input).id(getName("REST Authenticate"))
.setExchangePattern(ExchangePattern.InOut)
.process(new RestAuthProcessor()).id(getName(RestAuthProcessor.class.getSimpleName()));
}
public static void configureInstallRoute(RouteBuilder routeBuilder ,String input, String name){
routeBuilder.from(input).id(getName(name))
.setExchangePattern(ExchangePattern.InOut)
.process(new ClientAppProcessor()).id(getName(ClientAppProcessor.class.getSimpleName()));
}
public static void configureBackgroundTimer(RouteBuilder routeBuilder ,String input){
routeBuilder.from(input).id(getName("Declination"))
.process(new AisExpiryProcessor()).id(getName(AisExpiryProcessor.class.getSimpleName()))
.process(new SaveProcessor()).id(getName(SaveProcessor.class.getSimpleName()))
.process(new DeclinationProcessor()).id(getName(DeclinationProcessor.class.getSimpleName()))
.to("log:nz.co.fortytwo.signalk.model.update?level=DEBUG").end();
}
public static void configureAlarmsTimer(RouteBuilder routeBuilder ,String input){
routeBuilder.from(input).id(getName("Alarms"))
.process(new AlarmProcessor()).id(getName(AlarmProcessor.class.getSimpleName()))
.to("log:nz.co.fortytwo.signalk.model.update?level=DEBUG").end();
}
public static void configureNMEA0183Timer(RouteBuilder routeBuilder ,String input){
routeBuilder.from(input).id(getName("NMEA0183"))
.process(new NMEA0183ExportProcessor()).id(getName(NMEA0183ExportProcessor.class.getSimpleName()))
.to("log:nz.co.fortytwo.signalk.model.update?level=DEBUG").end();
}
public static void configureAnchorWatchTimer(RouteBuilder routeBuilder ,String input){
routeBuilder.from(input).id(getName("AnchorWatch"))
.process(new AnchorWatchProcessor()).id(getName(AnchorWatchProcessor.class.getSimpleName()))
.to("log:nz.co.fortytwo.signalk.model.update?level=DEBUG").end();
}
public static void configureWindTimer(RouteBuilder routeBuilder ,String input){
routeBuilder.from("timer://wind?fixedRate=true&period=1000").id(getName("True Wind"))
.process(new WindProcessor()).id(getName(WindProcessor.class.getSimpleName()))
.to("log:nz.co.fortytwo.signalk.model.update?level=DEBUG")
.end();
}
public static void configureDepthTimer(RouteBuilder routeBuilder ,String input){
routeBuilder.from("timer://depth?fixedRate=true&period=1000").id(getName("Depth"))
.process(new DepthProcessor()).id(getName(DepthProcessor.class.getSimpleName()))
.to("log:nz.co.fortytwo.signalk.model.update?level=DEBUG")
.end();
}
public static void configureCommonOut(RouteBuilder routeBuilder ) throws IOException{
routeBuilder.from(RouteManager.SEDA_COMMON_OUT).id(getName("COMMON_OUT"))
.onException(Exception.class).handled(true).maximumRedeliveries(0)
.to("log:nz.co.fortytwo.signalk.model.output?level=ERROR")
.end()
.process(new SourceRefToSourceProcessor()).id(getName(SourceRefToSourceProcessor.class.getSimpleName()))
.process(new MapToJsonProcessor()).id(getName(MapToJsonProcessor.class.getSimpleName()))
.process(new FullToDeltaProcessor()).id(getName(FullToDeltaProcessor.class.getSimpleName()))
.split().body()
//swap payloads from storage
//.process(new StorageProcessor()).id(getName(InputFilterProcessor.class.getSimpleName()))
.process(new OutputFilterProcessor()).id(getName(OutputFilterProcessor.class.getSimpleName()))
.multicast().parallelProcessing()
.to(RouteManager.DIRECT_TCP,
RouteManager.SEDA_WEBSOCKETS,
RouteManager.DIRECT_MQTT,
RouteManager.DIRECT_STOMP,
RouteManager.DIRECT_XMPP,
"log:nz.co.fortytwo.signalk.model.output?level=DEBUG"
).id(getName("Multicast Outputs"))
.end();
routeBuilder.from(RouteManager.DIRECT_MQTT).id(getName("MQTT out"))
.filter(routeBuilder.header(ConfigConstants.OUTPUT_TYPE).isEqualTo(ConfigConstants.OUTPUT_MQTT))
.process(new MqttProcessor()).id(getName(MqttProcessor.class.getSimpleName()))
.to(RouteManager.MQTT+"?publishTopicName=signalk.dlq").id(getName("MQTT Broker"));
routeBuilder.from(RouteManager.DIRECT_STOMP).id(getName("STOMP out"))
.filter(routeBuilder.header(ConfigConstants.OUTPUT_TYPE).isEqualTo(ConfigConstants.OUTPUT_STOMP))
.process(new StompProcessor()).id(getName(StompProcessor.class.getSimpleName()))
.to(RouteManager.STOMP).id(getName("STOMP Broker"));
routeBuilder.from(RouteManager.DIRECT_XMPP).id(getName("XMPP direct"))
.filter(routeBuilder.header(ConfigConstants.OUTPUT_TYPE).isEqualTo(ConfigConstants.OUTPUT_XMPP))
.to(RouteManager.SEDA_XMPP).id(getName("XMPP Broker"));
}
public static void configureSubscribeTimer(RouteBuilder routeBuilder ,Subscription sub) throws Exception{
String routeId = getRouteId(sub);
String input = "quartz2://"+routeId+"?trigger.repeatCount=-1&trigger.repeatInterval="+sub.getPeriod();
logger.info("Configuring route "+input);
//if(logger.isDebugEnabled())logger.debug("Configuring route "+input);
String wsSession = sub.getWsSession();
RouteDefinition route = routeBuilder.from(input);
route.process(new FullExportProcessor(wsSession,routeId)).id(FullExportProcessor.class.getSimpleName()+"-"+routeId)
.onException(Exception.class).handled(true).maximumRedeliveries(0)
.to("log:nz.co.fortytwo.signalk.model.output.subscribe?level=ERROR&maxChars=1000")
.end()
.setHeader(WebsocketConstants.CONNECTION_KEY, routeBuilder.constant(wsSession))
.to(RouteManager.SEDA_COMMON_OUT).id(getName("SEDA_COMMON_OUT"))
.end();
route.setId(routeId);
((DefaultCamelContext)CamelContextFactory.getInstance()).addRouteDefinition(route);
((DefaultCamelContext)CamelContextFactory.getInstance()).startRoute(route.getId());
//routeBuilder.getContext().startAllRoutes();
}
private static String getRouteId(Subscription sub) {
if(StringUtils.isBlank(sub.getRouteId())){
String routeId = getName("sub_"+sub.getWsSession());
sub.setRouteId(routeId);
}
return sub.getRouteId();
}
public static void removeSubscribeTimers(RouteManager routeManager, ConcurrentLinkedQueue<Subscription> subs) throws Exception {
for(Subscription sub : subs){
SignalkRouteFactory.removeSubscribeTimer(routeManager, sub);
}
}
public static void removeSubscribeTimer(RouteBuilder routeManager, Subscription sub) throws Exception {
RouteDefinition routeDef = ((DefaultCamelContext)routeManager.getContext()).getRouteDefinition(getRouteId(sub));
if(routeDef==null)return;
if(logger.isDebugEnabled())logger.debug("Stopping sub "+getRouteId(sub)+","+routeDef);
((DefaultCamelContext)routeManager.getContext()).stopRoute(routeDef);
if(logger.isDebugEnabled())logger.debug("Removing sub "+getRouteId(sub));
((DefaultCamelContext)routeManager.getContext()).removeRouteDefinition(routeDef);
if(logger.isDebugEnabled())logger.debug("Done removing sub "+getRouteId(sub));
}
public static void configureHeartbeatRoute(RouteBuilder routeBuilder, String input) {
routeBuilder.from(input).id(getName("Heartbeat"))
.onException(Exception.class).handled(true).maximumRedeliveries(0)
.to("log:nz.co.fortytwo.signalk.model.output.all?level=ERROR")
.end()
.process(new HeartbeatProcessor()).id(getName(HeartbeatProcessor.class.getSimpleName()));
}
public static String getName(String name) {
int c = 0;
String tmpName = name;
while(nameSet.contains(tmpName)){
tmpName=name+"-"+c;
c++;
}
nameSet.add(tmpName);
return tmpName;
}
public static void startLogRoutes(RouteBuilder routeBuilder, String host, int restPort) {
//list logs dir
routeBuilder.from(host + restPort + "/signalk/v1/listLogs?sessionSupport=true&matchOnUriPrefix=true").id("REST List logs")
.setExchangePattern(ExchangePattern.InOut)
.process(new Processor() {
@Override
public void process(Exchange exchange) throws Exception {
File dir = new File("signalk-static/logs");
exchange.getIn().setBody(Json.array(dir.list()));
}
});
routeBuilder.from(host + restPort + "/signalk/v1/getLogs?sessionSupport=true&matchOnUriPrefix=true").id("REST Get logs")
.setExchangePattern(ExchangePattern.InOut)
.process(new Processor() {
@Override
public void process(Exchange exchange) throws Exception {
String logFile = exchange.getIn().getHeader("logFile", String.class);
if(logFile.contains("/")){
logFile=logFile.substring(logFile.lastIndexOf("/")+1, logFile.length());
}
exchange.getIn().setHeader(Exchange.CONTENT_TYPE, "text/plain");
if(StringUtils.isBlank(logFile)){
exchange.getIn().setHeader(Exchange.HTTP_RESPONSE_CODE,
HttpServletResponse.SC_BAD_REQUEST);
exchange.getIn().setBody("Bad request");
}
File dir = new File("signalk-static/logs/"+logFile);
exchange.getIn().setBody(FileUtils.readFileToString(dir));
}
});
}
}