Mini framework MVC

O framework é baseado em duas classes: FrontController e Command.

Funcionamento básico

O seguinte diagrama de sequência mostra o funcionamento, sendo que o comando solicitado é Home que possui um método chamado sobre().

Definições das classes

package minimvc;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.regex.Pattern;

import static java.util.regex.Pattern.CASE_INSENSITIVE;

public class FrontController extends HttpServlet {
    private String commandPackage;
    private String paramName;
    private Authorization authorization;

    @Override
    public void init() throws ServletException {
        commandPackage = getInitParameter("commandPackage");
        if (commandPackage == null) {
            commandPackage = "commands";
        }

        paramName = getInitParameter("paramName");
        if (paramName == null) {
            paramName = "command";
        }

        try {
            // Tenta abrir o arquivo de configurações de autorização
            InputStream authConfig = getServletContext().getResourceAsStream("/WEB-INF/authorization.xml");
            if (authConfig == null) {
                return;
            }

            // Leitura das configurações de autorização
            DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
            Document doc = docBuilder.parse(authConfig);

            // Criação de estrutura de busca de permissões
            authorization = new Authorization();
            NodeList permissions = doc.getElementsByTagName("perm");

            for (int i = 0; i < permissions.getLength(); i++) {
                Element perm = (Element) permissions.item(i);

                String[] commands = perm.getTextContent().trim().split("\\s+");
                String[] groups = perm.getAttribute("name").trim().split("[\\s,]+");

                authorization.add(commands, groups);
            }
        }
        catch (ParserConfigurationException | SAXException | IOException e) {
            throw new ServletException(e);
        }
    }

    protected void processRequest(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // Executa um comando conforme a URL
        String cmdName = null;
        String cmdFunction = null;
        try {
            // Verifica o parâmetro do comando
            String fullCommand = request.getParameter(paramName);
            if (fullCommand == null) {
                response.sendError(422, "Comando não informado");
                return;
            }
            if (!isCommand(fullCommand)) {
                response.sendError(422, "Sintaxe de comando inválida: '" + fullCommand + "'");
                return;
            }

            // Obtém as partes do comando
            String[] cmdParams = fullCommand.split(":");
            cmdName = cmdParams[0];
            cmdFunction = cmdParams.length == 2 ? cmdParams[1] : "index";

            // Processo de autorização
            Collection groups = (Collection) request.getSession().getAttribute("groups");
            if (!authorization.isAuthorized(groups, cmdName, cmdFunction)) {
                response.sendError(403, "Usuário não autorizado");
                return;
            }

            // Inicia a instância do comando
            @SuppressWarnings("unchecked") Class<Command> c = (Class<Command>) Class.forName(commandPackage + "." + cmdName);
            Command command = c.newInstance();
            command.init(request, response);

            // Realiza a chamada do método do comando
            Method m = c.getMethod(cmdFunction);
            m.invoke(command);
        }
        // Se o comando ou método não existir, retorna HTTP 500
        catch (ClassNotFoundException e) {
            response.sendError(404, "Comando não encontrado: '" + cmdName + "'");
        } catch (IllegalAccessException | InstantiationException e) {
            response.sendError(404, "Falha ao iniciar comando: '" + cmdFunction + "'");
        } catch (NoSuchMethodException e) {
            response.sendError(404, "Método não encontrado: '" + cmdFunction + "'");
        } catch (InvocationTargetException e) {
            throw new ServletException(e.getCause());
        }
    }

    private static final Pattern COMMAND_RE = Pattern.compile("^[a-z_][a-z0-9_]*(?::[a-z_][a-z0-9_]*)?$", CASE_INSENSITIVE);

    private static boolean isCommand(String fullCommand) {
        return COMMAND_RE.matcher(fullCommand).matches();
    }

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        processRequest(request, response);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        processRequest(request, response);
    }

    private static class Authorization {
        final Map<String, Set<String>> permissions = new HashMap<>();

        void add(String[] commands, String[] groups) {
            for (String cmd : commands) {
                permissions.computeIfAbsent(cmd, k -> new HashSet<>())
                    .addAll(Arrays.asList(groups));
            }
        }

        public boolean isAuthorized(Collection userGroups, String cmdName, String cmdFunction) {
            if (userGroups == null) {
                userGroups = Collections.emptySet();
            }

            // Verifica se existe uma regra para o comando inteiro...
            Set<String> groups = permissions.get(cmdName);
            if (groups == null) {
                // ...não tendo, verifica se existe a regra "cmd:function"
                groups = permissions.get(cmdName + ":" + cmdFunction);

                // Não existindo nenhuma regra, está liberado.
                if (groups == null) {
                    return true;
                }
            }

            // Verifica se o comando atual está liberado para um dos grupos do usuário
            return !Collections.disjoint(groups, userGroups);
        }
    }
}

package minimvc;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

public abstract class Command {
    protected HttpServletRequest request;
    protected HttpServletResponse response;
    protected PrintWriter out;

    /**
     * Método interno usado por FrontController
     */
    final void init(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        this.request = request;
        this.response = response;
        this.out = response.getWriter();
    }

    // Métodos utilitários

    protected final void forward(String path) throws ServletException, IOException {
        RequestDispatcher rd = request.getRequestDispatcher(path);
        rd.forward(request, response);
    }

    protected final void redirect(String location) throws IOException {
        response.sendRedirect(location);
    }

    protected final String contextUrl(String relativePath) {
        if (relativePath == null) {
            relativePath = "";
        }
        return request.getContextPath() + relativePath;
    }

    protected final String contextUrl() {
        return contextUrl(null);
    }

    protected final Cookie getCookie(String name) {
        Cookie[] cookies = request.getCookies();

        for (Cookie ck : cookies) {
            if (ck.getName().equals(name)) {
                return ck;
            }
        }

        return null;
    }

    protected final void addCookie(Cookie cookie) {
        response.addCookie(cookie);
    }

    protected final String getParameter(String name) {
        return request.getParameter(name);
    }

    protected final Object getAttribute(String name) {
        return getAttribute(name, null);
    }

    protected final Object getAttribute(String name, String scope) {
        if (scope == null) {
            Object value = request.getAttribute(name);
            if (value != null) {
                return value;
            }

            value = request.getSession().getAttribute(name);
            if (value != null) {
                return value;
            }

            value = request.getServletContext().getAttribute(name);
            if (value != null) {
                return value;
            }

            return null;
        }

        switch (scope) {
            case "request":
                return request.getAttribute(name);

            case "session":
                return request.getSession().getAttribute(name);

            case "application":
                return request.getServletContext().getAttribute(name);

            default:
                throw new IllegalArgumentException("scope");
        }
    }

    protected final void setAttribute(String name, Object value) {
        setAttribute(name, value, null);
    }

    protected final void setAttribute(String name, Object value, String scope) {
        if (scope == null)
            scope = "request";

        switch (scope) {
            case "request":
                request.setAttribute(name, value);
                break;

            case "session":
                request.getSession().setAttribute(name, value);
                break;

            case "application":
                request.getServletContext().setAttribute(name, value);
                break;

            default:
                throw new IllegalArgumentException("scope");
        }
    }

    protected final void removeAttribute(String name) {
        removeAttribute(name, null);
    }

    protected final void removeAttribute(String name, String scope) {
        if (scope == null) {
            setAttribute(name, null, "request");
            setAttribute(name, null, "session");
            setAttribute(name, null, "application");
        } else {
            setAttribute(name, null, scope);
        }
    }
}