library angel_framework.http.server; import 'dart:async'; import 'dart:collection' show HashMap; import 'dart:convert'; import 'dart:io'; import 'package:angel_container/angel_container.dart'; import 'package:angel_http_exception/angel_http_exception.dart'; import 'package:angel_route/angel_route.dart'; import 'package:combinator/combinator.dart'; import 'package:logging/logging.dart'; import 'package:meta/meta.dart'; import 'package:tuple/tuple.dart'; import '../http/http.dart'; import 'request_context.dart'; import 'response_context.dart'; import 'routable.dart'; import 'service.dart'; final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)'); /// A function that configures an [Angel] server in some way. typedef FutureOr AngelConfigurer(Angel app); /// A function that asynchronously generates a view from the given path and data. typedef FutureOr ViewGenerator(String path, [Map data]); /// A powerful real-time/REST/MVC server class. class Angel extends Routable { static ViewGenerator noViewEngineConfigured = (String view, [Map data]) => 'No view engine has been configured yet.'; final List _children = []; final Map>>> handlerCache = new HashMap(); Router _flattened; AngelHttp _http; bool _isProduction; Angel _parent; /// A global Map of converters that can transform responses bodies. final Map, List>> encoders = {}; /// A function that is called on every request to create a [Zone], A.K.A an asynchronous /// execution context. /// /// The utility of zones in Angel is to safely catch errors without crashing the application; /// this also lets the driver wrap failures in instances of [AngelHttpException]. Future Function( HttpRequest request, RequestContext req, ResponseContext res) createZoneForRequest; final Map _preContained = {}; /// A middleware to inject a serialize on every request. String Function(dynamic) serializer; /// A [Map] of dependency data obtained via reflection. /// /// You may modify this [Map] yourself if you intend to avoid reflection entirely. Map get preContained => _preContained; /// Returns the [flatten]ed version of this router in production. Router get optimizedRouter => _flattened ?? this; /// Determines whether to allow HTTP request method overrides. bool allowMethodOverrides = true; /// All child application mounted on this instance. List get children => new List.unmodifiable(_children); final Map _controllers = {}; /// A set of [Controller] objects that have been loaded into the application. Map get controllers => _controllers; /// Indicates whether the application is running in a production environment. /// /// The criteria for this is the `ANGEL_ENV` environment variable being set to /// `'production'`. /// /// This value is memoized the first time you call it, so do not change environment /// configuration at runtime! bool get isProduction { return _isProduction ??= (Platform.environment['ANGEL_ENV'] == 'production'); } /// Returns the parent instance of this application, if any. Angel get parent => _parent; /// Outputs diagnostics and debug messages. Logger logger; /// Plug-ins to be called right before server startup. /// /// If the server is never started, they will never be called. final List startupHooks = []; /// Plug-ins to be called right before server shutdown. /// /// If the server is never [close]d, they will never be called. final List shutdownHooks = []; /// Always run before responses are sent. /// /// These will only not run if a response's `willCloseItself` is set to `true`. final List responseFinalizers = []; Container _container; /// A [Map] of application-specific data that can be accessed by any /// piece of code that can see this [Angel] instance. /// /// Packages like `package:angel_configuration` populate this map /// for you. final Map configuration = {}; /// When set to `true` (default: `false`), the request body will be parsed /// automatically; otherwise, you must call [RequestContext].parseBody() manually, /// or use `lazyBody()`. bool eagerParseRequestBodies = false; /// When set to `true`, the original body bytes will be stored /// on requests. `false` by default. bool keepRawRequestBuffers = false; /// A [Container] used to inject dependencies. Container get container => _container; /// A function that renders views. /// /// Called by [ResponseContext]@`render`. ViewGenerator viewGenerator = noViewEngineConfigured; /// The handler currently configured to run on [AngelHttpException]s. Function(AngelHttpException e, RequestContext req, ResponseContext res) errorHandler = (AngelHttpException e, RequestContext req, ResponseContext res) { if (!req.accepts('text/html', strict: true) && (req.accepts('application/json') || req.accepts('application/javascript'))) { res.json(e.toJson()); return; } res.headers['content-type'] = 'text/html'; res.statusCode = e.statusCode; res.write("${e.message}"); res.write("

${e.message}

    "); for (String error in e.errors) { res.write("
  • $error
  • "); } res.write("
"); res.end(); }; @override Route addRoute( String method, String path, RequestHandler handler, {Iterable middleware: const []}) { if (_flattened != null) { logger?.warning( 'WARNING: You added a route ($method $path) to the router, after it had been optimized.'); logger?.warning( 'This route will be ignored, and no requests will ever reach it.'); } return super.addRoute(method, path, handler, middleware: middleware ?? []); } @override mount(String path, Router router) { if (_flattened != null) { logger?.warning( 'WARNING: You added mounted a child router ($path) on the router, after it had been optimized.'); logger?.warning( 'This route will be ignored, and no requests will ever reach it.'); } return super.mount(path.toString(), router); } /// Loads some base dependencies into the service container. void bootstrapContainer() { if (runtimeType != Angel) { container.registerSingleton(this); } container.registerSingleton(this); container.registerSingleton(this); container.registerSingleton(this); } /// Shuts down the server, and closes any open [StreamController]s. /// /// The server will be **COMPLETELY DEFUNCT** after this operation! Future close() { Future.forEach(services.values, (Service service) { service.close(); }); super.close(); _container = null; viewGenerator = noViewEngineConfigured; _preContained.clear(); handlerCache.clear(); encoders.clear(); //_serializer = json.encode; _children.clear(); _parent = null; logger = null; startupHooks.clear(); shutdownHooks.clear(); responseFinalizers.clear(); _flattened = null; _http?.close(); return new Future.value(_http?.httpServer); } @override void dumpTree( {callback(String tree), String header: 'Dumping route tree:', String tab: ' ', bool showMatchers: false}) { if (isProduction) { _flattened ??= flatten(this); _flattened.dumpTree( callback: callback, header: header?.isNotEmpty == true ? header : (isProduction ? 'Dumping flattened route tree:' : 'Dumping route tree:'), tab: tab ?? ' '); } else { super.dumpTree( callback: callback, header: header?.isNotEmpty == true ? header : (isProduction ? 'Dumping flattened route tree:' : 'Dumping route tree:'), tab: tab ?? ' '); } } /// Shortcuts for adding converters to transform the response buffer/stream of any request. void injectEncoders(Map, List>> encoders) { this.encoders.addAll(encoders); } Future getHandlerResult(handler, RequestContext req, ResponseContext res) { if (handler is RequestHandler) { var result = handler(req, res); return getHandlerResult(result, req, res); } if (handler is Future) { return handler.then((result) => getHandlerResult(result, req, res)); } if (handler is Function) { var result = runContained(handler, req, res); return getHandlerResult(result, req, res); } if (handler is Stream) { return getHandlerResult(handler.toList(), req, res); } return new Future.value(handler); } /// Runs some [handler]. Returns `true` if request execution should continue. Future executeHandler( handler, RequestContext req, ResponseContext res) { return getHandlerResult(handler, req, res).then((result) { if (result == null) return false; else if (result is bool) { return result; } else if (result != null) { return res.serialize(result); } else return res.isOpen; }); } /// Attempts to find a property by the given name within this application. findProperty(key) { if (configuration.containsKey(key)) return configuration[key]; return parent != null ? parent.findProperty(key) : null; } /// Runs several optimizations, *if* [isProduction] is `true`. /// /// * Preprocesses all dependency injection, and eliminates the burden of reflecting handlers /// at run-time. /// * [flatten]s the route tree into a linear one. /// /// You may [force] the optimization to run, if you are not running in production. void optimizeForProduction({bool force: false}) { if (isProduction == true || force == true) { _isProduction = true; _add(v) { if (v is Function && !_preContained.containsKey(v)) { _preContained[v] = preInject(v); } } void _walk(Router router) { router.middleware.forEach(_add); router.routes.forEach((r) { r.handlers.forEach(_add); if (r is SymlinkRoute) _walk(r.router); }); } _flattened ??= flatten(this); _walk(_flattened); logger?.config('Angel is running in production mode.'); } } /// Run a function after injecting from service container. /// If this function has been reflected before, then /// the execution will be faster, as the injection requirements were stored beforehand. Future runContained( Function handler, RequestContext req, ResponseContext res) { return new Future.sync(() { if (_preContained.containsKey(handler)) { return handleContained(handler, _preContained[handler])(req, res); } return runReflected(handler, req, res); }); } /// Runs with DI, and *always* reflects. Prefer [runContained]. Future runReflected( Function handler, RequestContext req, ResponseContext res) { var h = handleContained(handler, _preContained[handler] = preInject(handler)); return new Future.sync(() => h(req, res)); // return closureMirror.apply(args).reflectee; } /// Applies an [AngelConfigurer] to this instance. Future configure(AngelConfigurer configurer) { return new Future.sync(() => configurer(this)); } /// Mounts the child on this router. If [routable] is `null`, /// then this method will add a handler as a global middleware instead. /// /// If the router is an [Angel] instance, all controllers /// will be copied, as well as services and response finalizers. /// /// [before] and [after] will be preserved. /// /// NOTE: The above will not be properly copied if [path] is /// a [RegExp]. @override use(path, [Router routable, String namespace = null]) { if (routable == null) return all('*', path); var head = path.toString().replaceAll(_straySlashes, ''); if (routable is Angel) { _children.add(routable.._parent = this); _preContained.addAll(routable._preContained); if (routable.responseFinalizers.isNotEmpty) { responseFinalizers.add((req, res) { if (req.path.replaceAll(_straySlashes, '').startsWith(head)) { for (var finalizer in routable.responseFinalizers) finalizer(req, res); } return new Future.value(true); }); } routable._controllers.forEach((k, v) { var tail = k.toString().replaceAll(_straySlashes, ''); _controllers['$head/$tail'.replaceAll(_straySlashes, '')] = v; }); routable.services.forEach((k, v) { var tail = k.toString().replaceAll(_straySlashes, ''); services['$head/$tail'.replaceAll(_straySlashes, '')] = v; }); } if (routable is Service) { routable.app = this; } return super.use(path, routable, namespace); } Angel({@required Reflector reflector}) { _container = new Container(reflector); bootstrapContainer(); // ignore: deprecated_member_use createZoneForRequest = defaultZoneCreator; } @deprecated Future defaultZoneCreator(request, req, res) { return new Future.value( new ZoneSpecification( print: (Zone self, ZoneDelegate parent, Zone zone, String line) { if (logger != null) { logger.info(line); } else { return parent.print(zone, line); } }, ), ); } }