vinegar/servlet/server.go
2023-08-01 16:06:43 -04:00

336 lines
9.5 KiB
Go

package servlet
import (
"encoding/json"
"fmt"
"geniuscartel.xyz/vinegar/vinegarUtil"
"log"
"net"
"net/http"
"regexp"
"strconv"
"time"
)
const (
defaultLruSize = int64(1024 * 1024 * 50)
ContentTypeHeaderKey = "Content-Type"
ContentEncodingHeaderKey = "Content-Encoding"
AcceptEncodingHeaderKey = "Accept-Encoding"
)
type ErrorResponse struct {
Code int
Message string
}
type (
// VinegarWebServlet is the main server struct that handles HTTP requests and routing.
// It contains the following key fields:
//
//Port - The TCP port to listen on for incoming requests. Should be a string like ":8080".
//
//Router - A VinegarWebRouter instance that handles routing requests to handlers.
//
//ErrorRoutes - A map of status code to TemplateRoute for custom error pages.
//
//interrupts - A channel used to signal shutdown/interrupts to the server.
//
//errors - A channel that the server writes errors to.
//
//The main methods are:
//
//ServeHTTP - Handles all incoming requests. Calls Router.RouteRequest to handle routing.
//
//AddErrorRoute - Adds a custom error route for the given status code.
//
//Start - Starts the HTTP server listening on Port. Returns any errors.
//
//Shutdown - Shuts down the server by sending a signal on interrupts.
//
//SendError - Helper to send an error response with custom status code and error template.
//
//Typical usage:
//
// srv := NewServlet(":8080")
// srv.AddErrorRoute(404, notFoundRoute)
// srv.Router.AddRoute(someRoute)
// srv.Start()
// // ...
// srv.Shutdown()
VinegarWebServlet struct {
Port string
Router VinegarWebRouter
// ErrorRoutes is a map that stores TemplateRoute pointers
// for different HTTP status code error pages.
//
// It gets populated when custom error routes are defined via AddErrorRoute.
// For example:
//
// err404 := NewTemplateRoute(/*...*/)
// srv.AddErrorRoute(404, err404)
//
// This will add err404 to ErrorRoutes mapped to 404.
//
// Routes can then be rendered like:
//
// tmpl, exists := s.ErrorRoutes[404]
// if exists {
// // render tmpl
// }
//
// So ErrorRoutes provides a lookup from status code
// to the associated custom error route.
//
// It allows easily rendering custom error pages
// for different status codes.
ErrorRoutes map[int]*TemplateRoute
interrupts chan struct{}
errors chan error
}
VinegarHandlerFunction func(w http.ResponseWriter, req *http.Request)
)
// NewServlet creates and initializes a new VinegarWebServlet instance.
//
// It accepts the TCP port to listen on as a string parameter.
//
// It initializes an empty ErrorRoutes map to store custom error routes.
//
// It creates an interrupts channel to handle shutdown signals.
//
// It returns a pointer to the initialized VinegarWebServlet struct.
//
// Example usage:
//
// port := ":8080"
// srv := NewServlet(port)
func NewServlet(port string) *VinegarWebServlet {
errs := make(map[int]*TemplateRoute)
srv := VinegarWebServlet{Port: port, ErrorRoutes: errs, interrupts: make(chan struct{}, 500)}
return &srv
}
// NewServletRoute creates a new VinegarWebRoute instance.
//
// It accepts a routePattern string and handleFunc function as parameters:
//
// - routePattern is a regex string that will be compiled into the route Pattern.
//
// - handleFunc is the handler function to call when the route matches.
//
// It compiles the routePattern into a regexp.Pattern.
//
// It creates a VinegarWebRoute struct initialized with the Pattern,
// the handler function, and a new LRU cache.
//
// The returned VinegarWebRoute pointer can be added to a router to handle requests
// that match the routePattern.
func NewServletRoute(routePattern string, handleFunc VinegarHandlerFunction) *VinegarWebRoute {
pattern := regexp.MustCompile(routePattern)
route := VinegarWebRoute{Pattern: pattern, Handler: handleFunc, Cache: vinegarUtil.NewLRU(defaultLruSize)}
return &route
}
// AddErrorRoute adds a custom error route for the given status code.
//
// It accepts two parameters:
//
// - code: The HTTP status code this error route should handle.
//
// - route: A pointer to the TemplateRoute to use for that status code.
//
// It adds the route to the ErrorRoutes map, mapped to the status code.
//
// This allows custom error handling routes to be defined for different
// HTTP status codes. The ErrorRoutes map can then be checked to see if
// a custom error template has been defined for a given code.
//
// For example:
//
// err404 := NewTemplateRoute(...)
// srv.AddErrorRoute(404, err404)
//
// // Later...
// src.SendError(404)
func (s *VinegarWebServlet) AddErrorRoute(code int, route *TemplateRoute) {
route.Announce()
s.ErrorRoutes[code] = route
}
// ServeHTTP handles all incoming HTTP requests.
//
// It is called by the net/http server whenever a new request comes in.
//
// It takes an http.ResponseWriter and *http.Request as parameters.
//
// It calls the router's RouteRequest method to try to find a matching
// route and execute the handler.
//
// If routing fails, it returns a 404 Not Found response.
//
// This is the main request handler that handles routing and responding
// to all incoming requests. The VinegarWebServlet implements the
// http.Handler interface so it can be used by the net/http server.
//
func (s *VinegarWebServlet) ServeHTTP(w http.ResponseWriter, req *http.Request) {
err := s.Router.RouteRequest(w, req)
if err != nil {
http.NotFound(w, req)
}
}
// Start starts the VinegarWebServlet HTTP server listening on the configured port.
//
// It first checks that there is at least one route configured, otherwise it
// logs a fatal error and exits.
//
// It then calls net.Listen() to listen on the TCP port. Any errors are returned.
//
// It spawns a new goroutine to handle serving HTTP requests via http.Serve().
// Any errors from http.Serve() are sent on the s.errors channel.
//
// The interrupts channel in VinegarWebServlet is used to gracefully shutdown the HTTP server when needed
// If VinegarWebServlet.interrupts receives a signal via VinegarWebServlet.Shutdown(), the listener is closed and the server exits.
//
// Any errors from starting the listener or serving requests are returned.
//
func (s *VinegarWebServlet) Start() error {
if len(s.Router.Routes) < 1 {
log.Fatal("No routes found for server. Nothing to listen and serve.")
}
l, listenErr := net.Listen("tcp", s.Port)
if listenErr != nil {
return listenErr
}
go func() {
err := http.Serve(l, s)
if err != nil {
s.errors <- err
}
}()
log.Printf("Listening on [%s]\n", s.Port)
running := true
for running {
select {
case err := <-s.errors:
log.Printf("server on port %s failed: %v", s.Port, err)
case <-s.interrupts:
err := l.Close()
if err != nil {
return err
}
running = false
default:
time.Sleep(5)
}
}
return nil
}
// Shutdown gracefully shuts down the VinegarWebServlet HTTP server.
//
// It sends a signal on the s.interrupts channel, which signals the
// server's main loop to exit and stop listening/serving.
//
// This allows cleanly shutting down the HTTP server on demand without
// having to force kill any processes.
//
//Currently, this does not wait for outstanding responses to finish.
//
// Typical usage:
//
// srv := NewServlet(":8080")
// // ...start server
//
// // Later, to shutdown
// srv.Shutdown()
//
func (s *VinegarWebServlet) Shutdown() {
s.interrupts <- struct{}{}
}
// Announce logs a message when a VinegarWebRoute is added to the router.
//
// It does not accept any parameters.
//
// It prints a log message containing the route's URL pattern string.
// This indicates a new route was added.
//
// For example:
//
// route := NewServletRoute("/users", handler)
// router.AddRoute(route)
//
// // Prints:
// // Added route for [/users]
//
func (r *VinegarWebRoute) Announce() {
log.Printf("Added route for [%s]\n", r.Pattern.String())
}
// SendError sends an error response to the client with a custom status code
// and error message.
//
// It takes the following parameters:
//
// w - http.ResponseWriter to write the response to
// req - *http.Request that is causing the error
// code - int HTTP status code for the error response
// msg - string message describing the error
// aErr - error instance with more details on the error
//
// It first logs the error details.
//
// It then checks if there is a custom ErrorRoute defined for the status code.
// If so, it renders that template and writes it to the response.
//
// If not, it calls http.Error to send a generic error response.
//
// In both cases it sets the appropriate HTTP status code.
//
// This allows customizing error pages for different status codes,
// while falling back to default Go error handling.
func (s *VinegarWebServlet) SendError(w http.ResponseWriter, req *http.Request, code int, msg string, aErr error) {
log.Printf("[%d][%s]. Rendering template for code %d with message: %s\n", code, req.URL.Path, code, msg)
out, _ := json.Marshal(aErr)
log.Println(string(out))
tmpl, exists := s.ErrorRoutes[code]
if exists {
err := tmpl.TemplateManager.AddMixin("code", strconv.Itoa(code))
if err != nil {
http.Error(w, msg, code)
return
}
err = tmpl.TemplateManager.AddMixin("msg", msg)
if err != nil {
http.Error(w, msg, code)
return
}
w.WriteHeader(code)
bitties, err := tmpl.TemplateManager.RenderTemplate(fmt.Sprintf("%d.html", code))
if err != nil {
http.Error(w, msg, code)
return
}
_, err = w.Write([]byte(bitties))
if err != nil {
http.Error(w, msg, code)
return
}
return
}
http.Error(w, msg, code)
return
}