package act.xio;
/*-
* #%L
* ACT Framework
* %%
* Copyright (C) 2014 - 2017 ActFramework
* %%
* 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.
* #L%
*/
import act.Act;
import act.app.ActionContext;
import act.app.App;
import act.app.util.NamedPort;
import act.handler.RequestHandler;
import act.handler.builtin.AlwaysNotFound;
import act.handler.builtin.StaticFileGetter;
import act.handler.builtin.StaticResourceGetter;
import act.handler.builtin.controller.FastRequestHandler;
import act.handler.builtin.controller.RequestHandlerProxy;
import act.metric.Metric;
import act.metric.MetricInfo;
import act.metric.Timer;
import act.route.Router;
import act.util.DestroyableBase;
import act.view.ActErrorResult;
import org.osgl.$;
import org.osgl.Osgl;
import org.osgl.exception.NotAppliedException;
import org.osgl.http.H;
import org.osgl.logging.LogManager;
import org.osgl.logging.Logger;
import org.osgl.mvc.result.ErrorResult;
import org.osgl.mvc.result.NotFound;
import org.osgl.mvc.result.Result;
import org.osgl.util.E;
import org.osgl.util.S;
/**
* A `NetworkHandler` can be registered to an {@link Network} and get invoked when
* there are network event (e.g. an HTTP request) incoming
*/
public class NetworkHandler extends DestroyableBase {
private static Logger logger = LogManager.get(NetworkHandler.class);
final private App app;
private NamedPort port;
private Metric metric;
private $.Func2<H.Request, String, String> contentSuffixProcessor;
private $.Func2<H.Request, String, String> urlContextProcessor;
public NetworkHandler(App app) {
E.NPE(app);
this.app = app;
this.metric = Act.metricPlugin().metric("act.http");
this.initUrlProcessors();
}
private void initUrlProcessors() {
this.contentSuffixProcessor = app.config().contentSuffixAware() ? new ContentSuffixSensor() : DUMB_CONTENT_SUFFIX_SENSOR;
String urlContext = app.config().urlContext();
if (null != urlContext) {
this.urlContextProcessor = new UrlContextProcessor(urlContext);
} else {
this.urlContextProcessor = DUMB_CONTENT_SUFFIX_SENSOR;
}
}
public NetworkHandler(App app, NamedPort port) {
this(app);
this.port = port;
}
public App app() {
return app;
}
public void handle(final ActionContext ctx, NetworkDispatcher dispatcher) {
if (isDestroyed()) {
return;
}
final H.Request req = ctx.req();
String url = req.url();
H.Method method = req.method();
Exception refreshError = null;
if (Act.isDev()) {
try {
boolean updated = app.checkUpdates(false);
if (updated) {
initUrlProcessors();
}
} catch (Exception e) {
refreshError = e;
}
}
url = contentSuffixProcessor.apply(req, url);
try {
url = urlContextProcessor.apply(req, url);
} catch (NotFound notFound) {
ctx.handler(AlwaysNotFound.INSTANCE);
ctx.saveLocal();
AlwaysNotFound.INSTANCE.apply(ctx);
return;
}
Timer timer = metric.startTimer(MetricInfo.ROUTING);
final RequestHandler requestHandler = router().getInvoker(method, url, ctx);
ctx.handler(requestHandler);
timer.stop();
boolean resourceGetter = requestHandler instanceof StaticResourceGetter || requestHandler instanceof StaticFileGetter;
if (null != refreshError && !resourceGetter) {
ctx.saveLocal();
handleException(refreshError, ctx, "Error refreshing app");
ActionContext.clearCurrent();
return;
}
NetworkJob job = new NetworkJob() {
@Override
public void run() {
String key = S.concat(MetricInfo.HTTP_HANDLER, ":", requestHandler.toString());
Timer timer = metric.startTimer(key);
ctx.saveLocal();
try {
requestHandler.handle(ctx);
} catch (Result r) {
if (isError(r)) {
ctx.handler(FastRequestHandler.DUMB);
}
try {
r = RequestHandlerProxy.GLOBAL_AFTER_INTERCEPTOR.apply(r, ctx);
} catch (Exception e) {
logger.error(e, "Error calling global after interceptor");
r = ActErrorResult.of(e);
}
if (null == ctx.handler() || isError(r)) {
ctx.handler(FastRequestHandler.DUMB);
}
H.Format fmt = req.accept();
if (H.Format.UNKNOWN == fmt) {
fmt = req.contentType();
}
ctx.resp().addHeaderIfNotAdded(H.Header.Names.CONTENT_TYPE, fmt.contentType());
r.apply(req, ctx.resp());
} catch (Exception e) {
handleException(e, ctx, "Error handling network request");
} finally {
// we don't destroy ctx here in case it's been passed to
// another thread
ActionContext.clearCurrent();
if (null != timer) {
timer.stop();
}
}
}
};
if (method.unsafe() || !requestHandler.express(ctx)) {
dispatcher.dispatch(job);
} else {
job.run();
}
}
private boolean isError(Result r) {
return r instanceof ErrorResult;
}
private void handleException(Exception exception, final ActionContext ctx, String errorMessage) {
logger.error(exception, errorMessage);
Result r;
try {
r = RequestHandlerProxy.GLOBAL_EXCEPTION_INTERCEPTOR.apply(exception, ctx);
} catch (Exception e) {
logger.error(e, "Error calling global exception interceptor");
r = ActErrorResult.of(e);
}
if (null == r) {
r = ActErrorResult.of(exception);
} else if (r instanceof ErrorResult) {
r = ActErrorResult.of(r);
}
if (null == ctx.handler()) {
ctx.handler(FastRequestHandler.DUMB);
}
r.apply(ctx.req(), ctx.resp());
}
@Override
public String toString() {
return app().name();
}
private Router router() {
return app.router(port);
}
private static $.Func2<H.Request, String, String> DUMB_CONTENT_SUFFIX_SENSOR = new $.Func2<H.Request, String, String>() {
@Override
public String apply(H.Request request, String s) throws NotAppliedException, Osgl.Break {
return s;
}
};
static class UrlContextProcessor implements $.Func2<H.Request, String, String> {
private String context;
private int contextLen;
UrlContextProcessor(String context) {
this.context = context;
this.contextLen = context.length();
}
@Override
public String apply(H.Request request, String s) throws NotAppliedException, Osgl.Break {
if (s.length() < contextLen || !s.startsWith(context)) {
throw NotFound.get();
}
return s.substring(contextLen, s.length());
}
}
/**
* Process URL suffix based on suffix
*/
static class ContentSuffixSensor implements $.Func2<H.Request, String, String> {
private static final char[] mp3 = {'m', 'p'};
private static final char[] mp4 = {'m', 'p'};
private static final char[] mpa = {'m', 'p'};
private static final char[] pdf = {'p'};
private static final char[] gif = {};
private static final char[] tif = {};
private static final char[] png = {'p'};
private static final char[] jpg = {};
private static final char[] mpg = {};
private static final char[] svg = {'s'};
private static final char[] avi = {'a', 'v'};
private static final char[] xml = {'x', 'm'};
private static final char[] json = {'j', 's', 'o'};
private static final char[] ico = {'i', 'c'};
private static final char[] bmp = {'b', 'm'};
private static final char[] xls = {'x', 'l'};
private static final char[] wav = {'w'};
private static final char[] flv = {'f'};
private static final char[] csv = {'c'};
private static final char[] mov = {'m'};
private static final char[] xlsx = {'x', 'l', 's'};
@Override
public String apply(H.Request req, String url) throws NotAppliedException, Osgl.Break {
$.Var<H.Format> fmtBag = $.var();
String processedUrl = process(url, fmtBag);
H.Format fmt = fmtBag.get();
if (null != fmt) {
req.accept(fmt);
}
return processedUrl;
}
static String process(String url, $.Var<H.Format> fmtBag) {
int sz = url.length();
if (sz < 4) {
return url;
}
int start = sz - 1;
char c = url.charAt(start);
int initPos = 1;
char[] trait;
int sepPos = 3;
H.Format fmt = H.Format.JSON;
switch (c) {
case '3':
trait = mp3;
break;
case '4':
trait = mp4;
break;
case 'a':
trait = mpa;
break;
case 'f':
c = url.charAt(start - 1);
initPos = 2;
switch (c) {
case 'd':
trait = pdf;
fmt = H.Format.PDF;
break;
case 'i':
c = url.charAt(start - 2);
initPos = 3;
switch (c) {
case 'g':
trait = gif;
fmt = H.Format.GIF;
break;
case 't':
trait = tif;
fmt = H.Format.TIF;
break;
default:
return url;
}
break;
default:
return url;
}
break;
case 'g':
c = url.charAt(start - 1);
initPos = 2;
switch (c) {
case 'n':
trait = png;
fmt = H.Format.PNG;
break;
case 'p':
c = url.charAt(start - 2);
initPos = 3;
switch (c) {
case 'j':
trait = jpg;
fmt = H.Format.JPG;
break;
case 'm':
trait = mpg;
fmt = H.Format.MPG;
break;
default:
return url;
}
break;
case 'v':
trait = svg;
fmt = H.Format.SVG;
break;
default:
return url;
}
break;
case 'i':
trait = avi;
fmt = H.Format.AVI;
break;
case 'l':
trait = xml;
fmt = H.Format.XML;
break;
case 'n':
sepPos = 4;
trait = json;
break;
case 'o':
trait = ico;
fmt = H.Format.ICO;
break;
case 'p':
trait = bmp;
fmt = H.Format.BMP;
break;
case 's':
trait = xls;
fmt = H.Format.XLS;
break;
case 'v':
c = url.charAt(start - 1);
initPos = 2;
switch (c) {
case 'a':
trait = wav;
fmt = H.Format.WAV;
break;
case 'l':
trait = flv;
fmt = H.Format.FLV;
break;
case 's':
trait = csv;
fmt = H.Format.CSV;
break;
case 'o':
trait = mov;
fmt = H.Format.MOV;
break;
default:
return url;
}
break;
case 'x':
sepPos = 4;
trait = xlsx;
fmt = H.Format.XLSX;
break;
default:
return url;
}
char sep = url.charAt(start - sepPos);
if (sep != '/') {
return url;
}
for (int i = initPos; i < sepPos; ++i) {
if (url.charAt(start - i) != trait[sepPos - i - 1]) {
return url;
}
}
fmtBag.set(fmt);
return url.substring(0, sz - sepPos - 1);
}
}
}