1
\$\begingroup\$

I have this web application for sharing temporary notes. It supports HTML markup (<p>, <code>, etc.) along the TeX code.

(An example document)

Config.java

package net.coderodde.roddenotes.config;

/**
 * This class contains the general application parameters.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Dec 15, 2017)
 */
public final class Config {

    /**
     * This class contains form parameter names.
     */
    public static final class PARAMETERS {

        /**
         * The name of the parameter holding the document ID.
         */
        public static final String DOCUMENT_ID = "documentId";

        /**
         * The name of the parameter holding the edit token.
         */
        public static final String EDIT_TOKEN = "editToken";

        /**
         * The name of the parameter holding the document text.
         */
        public static final String DOCUMENT_TEXT = "documentText";
    }

    /**
     * This class contains attribute names.
     */
    public static final class ATTRIBUTES {

        /**
         * Used for communicating the actual document text.
         */
        public static final String DOCUMENT_TEXT = "documentText";

        /**
         * Used for communicating publish links.
         */
        public static final String PUBLISH_LINK = "publishLink";

        /**
         * Used for communicating document IDs.
         */
        public static final String DOCUMENT_ID = "documentId";

        /**
         * Used for communicating edit tokens.
         */
        public static final String EDIT_TOKEN = "editToken";
    }

    public static final class PAGES {

        /**
         * The name of the JSP file for viewing a (non-editable) document.
         */
        public static final String VIEW_PAGE = "view.jsp";

        /**
         * The name of the JSP file for editing documents.
         */
        public static final String EDITOR_PAGE = "edit.jsp";

        /**
         * The name of the JSP file rendered upon missing document.
         */
        public static final String NO_DOCUMENT_PAGE = "viewDocNotFound.jsp";

        /**
         * The name of the HTML file rendered upon requesting a view without the
         * document ID parameter.
         */
        public static final String NO_ID_VIEW_PAGE = "viewIdNotGiven.html";

        /**
         * The name of the HTML file rendered whenever receiving a request where
         * the document ID and the edit token do not match.
         */
        public static final String DONT_HACK_US_PAGE = "dontHackUs.html";
    }

    /**
     * This class contains all the error messages in the application.
     */
    public static final class ERROR_MESSAGES {

        /**
         * The name of the CSS class used for rendering error messages.
         */
        public static final String ERROR_MESSAGE_CSS_CLASS = "error";

        /**
         * The opening span tag.
         */
        public static final String SPAN_BEGIN = 
                "<span class='" +
                ERROR_MESSAGE_CSS_CLASS + 
                "'>";

        /**
         * The closing span tag.
         */
        public static final String SPAN_END = "</span>";

        /**
         * The text rendered whenever the document with given ID does not exist.
         */
        public static final String NO_SUCH_DOCUMENT_TEXT_FORMAT = 
                SPAN_BEGIN + "(Document with ID %s does not exist.)" + SPAN_END;

        /**
         * The text rendered whenever the user accesses the view page without a any
         * document ID.
         */
        public static final String NO_GIVEN_ID_TEXT = 
                SPAN_BEGIN + 
                "(Cannot find a document without an ID.)" +
                SPAN_END;
    }

    public static final class STATUS_MESSAGES {

        public static final String SUCCESS = "success";
        public static final String FAILURE = "failure";
    }
}

DeleteDocumentServlet.java

package net.coderodde.roddenotes.controllers;

import java.io.IOException;
import java.io.PrintWriter;
import java.sql.SQLException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.coderodde.roddenotes.config.Config;
import net.coderodde.roddenotes.model.Document;
import net.coderodde.roddenotes.sql.support.MySQLDataAccessObject;

/**
 * This servlet is responsible for deleting documents from the database.
 *
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Dec 16, 2017)
 */
@WebServlet(name = "DeleteDocumentServlet", urlPatterns = {"/deleteDocument"})
public class DeleteDocumentServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest request,
                          HttpServletResponse response)
            throws ServletException, IOException {
        try (PrintWriter out = response.getWriter()) {
            String documentId = 
                    request.getParameter(Config.PARAMETERS.DOCUMENT_ID);

            String editToken = 
                    request.getParameter(Config.PARAMETERS.EDIT_TOKEN);

            if (documentId == null || editToken == null) {
                out.print(Config.STATUS_MESSAGES.FAILURE);
                return;
            }

            Document document = null;

            try {
                document = MySQLDataAccessObject.INSTANCE.getDocument(documentId);
            } catch (SQLException ex) {
                out.print(Config.STATUS_MESSAGES.FAILURE);
                throw new RuntimeException(ex);
            }

            if (document == null) {
                out.print(Config.STATUS_MESSAGES.FAILURE);
                return;
            }

            if (!document.getEditToken().equals(editToken)) {
                out.print(Config.STATUS_MESSAGES.FAILURE);
                return;
            }

            try {
                MySQLDataAccessObject.INSTANCE.deleteDocument(documentId);
                out.print(Config.STATUS_MESSAGES.SUCCESS);
            } catch (SQLException ex) {
                throw new RuntimeException(ex);
            }
        }
    }
}

EditServlet.java

package net.coderodde.roddenotes.controllers;

import java.io.IOException;
import java.io.PrintWriter;
import java.sql.SQLException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.coderodde.roddenotes.config.Config;
import net.coderodde.roddenotes.config.Config.ATTRIBUTES;
import net.coderodde.roddenotes.model.Document;
import net.coderodde.roddenotes.sql.support.MySQLDataAccessObject;
import static net.coderodde.roddenotes.util.MiscellaneousUtilities.getServerURL;

/**
 * This servlet handles the edit requests. If the servlet receives parameters 
 * defining the document ID and its edit token, and both are valid, this servlet
 * prepares an editor view for the document. Otherwise, a new document is
 * created and is presented to the user.
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Dec 15, 2017)
 */
@WebServlet(name = "EditServlet", urlPatterns = {"/edit"})
public class EditServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, 
                         HttpServletResponse response)
            throws ServletException, IOException {
        String id = request.getParameter(Config.PARAMETERS.DOCUMENT_ID);
        String editToken = request.getParameter(Config.PARAMETERS.EDIT_TOKEN);

        if (id == null || editToken == null) {
            serveFreshEmptyDocument(request, response);
            return;
        }

        Document document = null;

        try {
            document = MySQLDataAccessObject.INSTANCE.getDocument(id);
        } catch (SQLException ex) {
            throw new RuntimeException(ex);
        }

        if (document == null) {
            serveFreshEmptyDocument(request, response);
            return;
        }

        if (!document.getEditToken().equals(editToken)) {
            serveFreshEmptyDocument(request, response);
            return;
        }

        request.setAttribute(ATTRIBUTES.DOCUMENT_ID, document.getId());
        request.setAttribute(ATTRIBUTES.EDIT_TOKEN, document.getEditToken());
        request.setAttribute(ATTRIBUTES.DOCUMENT_TEXT, document.getText());
        request.setAttribute(ATTRIBUTES.PUBLISH_LINK, 
                             getPublishLink(request, document));

        request.getRequestDispatcher(Config.PAGES.EDITOR_PAGE)
               .forward(request, response);
    }

    @Override
    protected void doPost(HttpServletRequest request, 
                          HttpServletResponse response)
            throws ServletException, IOException {
        try (PrintWriter out = response.getWriter()) {
            out.println("This servlet does not serve POST requests.");
        }
    }

    private String getPublishLink(HttpServletRequest request, 
                                  Document document) {
        return new StringBuilder(getServerURL(request))
                .append("/view?")
                .append(Config.PARAMETERS.DOCUMENT_ID)
                .append('=')
                .append(document.getId())
                .toString();
    }

    private void serveFreshEmptyDocument(HttpServletRequest request,
                                         HttpServletResponse response)
    throws ServletException, IOException {
        Document document = null;

        try {
            document = MySQLDataAccessObject.INSTANCE.createNewDocument();
        } catch (SQLException ex) {
            throw new RuntimeException(ex);
        }

        String path = getPath(request, document);
        request.getRequestDispatcher(path).forward(request, response);
    }

    private String getPath(HttpServletRequest request, Document document) {
        return new StringBuilder().append(request.getPathInfo())
                                  .append('?')
                                  .append(Config.PARAMETERS.DOCUMENT_ID)
                                  .append('=')
                                  .append(document.getId())
                                  .append('&')
                                  .append(Config.PARAMETERS.EDIT_TOKEN)
                                  .append('=')
                                  .append(document.getEditToken())
                                  .toString();
    }
}

HomeServlet.java

package net.coderodde.roddenotes.controllers;

import java.io.IOException;
import java.io.PrintWriter;
import java.sql.SQLException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.coderodde.roddenotes.config.Config;
import net.coderodde.roddenotes.model.Document;
import net.coderodde.roddenotes.sql.support.MySQLDataAccessObject;

/**
 * This servlet listens to the root resource of this application, creates a new
 * document and redirects to the document's edit view.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Dec 15, 2017)
 */
@WebServlet(name = "HomeServlet", urlPatterns = {""})
public class HomeServlet extends HttpServlet {

    private static final String EDIT_SERVLET_NAME = "edit";

    @Override
    protected void doGet(HttpServletRequest request, 
                         HttpServletResponse response)
            throws ServletException, IOException {
        Document document = null;

        try {
            document = MySQLDataAccessObject.INSTANCE.createNewDocument();         
        } catch (SQLException ex) {
            throw new RuntimeException(ex);
        }

        if (document == null) {
            throw new NullPointerException("Creating a document failed.");
        }

        response.sendRedirect(getEditPageAddress(document));
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        try (PrintWriter out = response.getWriter()) {
            out.print("Please access this resource via GET method.");
        }
    }

    /**
     * Constructs the address for the edit page.
     * 
     * @param document the document to prepare for editing.
     * @return a page address relative to the web application.
     */
    private String getEditPageAddress(Document document) {
        return new StringBuilder()
                   .append(EDIT_SERVLET_NAME)
                   .append('?')
                   .append(Config.PARAMETERS.DOCUMENT_ID)
                   .append('=')
                   .append(document.getId())
                   .append('&')
                   .append(Config.PARAMETERS.EDIT_TOKEN)
                   .append('=')
                   .append(document.getEditToken())
                   .toString();
    }
}

UpdateDocumentServlet.java

package net.coderodde.roddenotes.controllers;

import java.io.IOException;
import java.io.PrintWriter;
import java.sql.SQLException;
import java.util.regex.Pattern;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.coderodde.roddenotes.config.Config;
import net.coderodde.roddenotes.config.Config.PAGES;
import net.coderodde.roddenotes.config.Config.PARAMETERS;
import net.coderodde.roddenotes.model.Document;
import net.coderodde.roddenotes.sql.support.MySQLDataAccessObject;

/**
 * This servlet is responsible for updating an existing document. If the
 * incoming document is not yet in the database, it is put there.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Dec 15, 2017)
 */
@WebServlet(name = "UpdateDocumentServlet", urlPatterns = {"/update"})
public class UpdateDocumentServlet extends HttpServlet {

    /**
     * The regular expression for the begin script tag.
     */ 
    private static final String SCRIPT_TAG_BEGIN_REGEX = "<\\s*script\\s*>";

    /**
     * The regular expression for the end script tag.
     */ 
    private static final String SCRIPT_TAG_END_REGEX = "<\\s*/\\s*script\\s*>";

    private static final String SCRIPT_TAG_BEGIN_SUBSTITUTE = "&lt;script&gt;";
    private static final String SCRIPT_TAG_END_SUBSTITUTE = "&lt;/script&gt;";

    @Override
    protected void doGet(HttpServletRequest request,
                         HttpServletResponse response)
    throws ServletException, IOException {
        try (PrintWriter out = response.getWriter()) {
            out.println("This servlet does not work via GET method.");
        }
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String documentId   = request.getParameter(PARAMETERS.DOCUMENT_ID);
        String documentText = request.getParameter(PARAMETERS.DOCUMENT_TEXT);
        String editToken    = request.getParameter(PARAMETERS.EDIT_TOKEN);

        documentText = sanitizeText(documentText);

        try (PrintWriter out = response.getWriter()) {
            if (documentId == null 
                    || editToken == null 
                    || documentText == null) {
                out.print(Config.STATUS_MESSAGES.FAILURE);
                return;
            }

            Document document = new Document();
            document.setId(documentId);
            document.setEditToken(editToken);
            document.setText(documentText);

            try {
                boolean validUpdate = 
                        MySQLDataAccessObject.INSTANCE.updateDocument(document);

                if (!validUpdate) {
                    request.getRequestDispatcher(PAGES.DONT_HACK_US_PAGE)
                           .forward(request, response);
                    return;
                }

                out.print(Config.STATUS_MESSAGES.SUCCESS);
            } catch (SQLException ex) {
                throw new RuntimeException(ex);
            }
        }
    }

    private String sanitizeText(String text) {
        Pattern patternBeginTag = 
                Pattern.compile(SCRIPT_TAG_BEGIN_REGEX,
                                Pattern.CASE_INSENSITIVE);

        Pattern patternEndTag =
                Pattern.compile(SCRIPT_TAG_END_REGEX,
                                Pattern.CASE_INSENSITIVE);

        text = patternBeginTag.matcher(text)
                              .replaceAll(SCRIPT_TAG_BEGIN_SUBSTITUTE);

        return patternEndTag.matcher(text)
                            .replaceAll(SCRIPT_TAG_END_SUBSTITUTE);
    }
}

ViewServlet.java

package net.coderodde.roddenotes.controllers;

import java.io.IOException;
import java.io.PrintWriter;
import java.sql.SQLException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.coderodde.roddenotes.config.Config;
import net.coderodde.roddenotes.model.Document;
import net.coderodde.roddenotes.sql.support.MySQLDataAccessObject;

/**
 * This servlet is responsible for showing the documents via their ID.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Dec 15, 2017)
 */
@WebServlet(name = "ViewServlet", urlPatterns = {"/view"})
public class ViewServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, 
                         HttpServletResponse response)
            throws ServletException, IOException {
        String documentId = 
                request.getParameter(Config.PARAMETERS.DOCUMENT_ID);

        if (documentId == null) {
            request.getRequestDispatcher(Config.PAGES.NO_ID_VIEW_PAGE)
                   .forward(request, response);
            return;
        }

        Document document = null;

        try {
            document = MySQLDataAccessObject
                        .INSTANCE
                        .getDocument(documentId);
        } catch (SQLException ex) {
            throw new RuntimeException(ex);
        }

        if (document == null) {
            request.setAttribute(Config.ATTRIBUTES.DOCUMENT_ID,
                                 documentId);
            request.getRequestDispatcher(Config.PAGES.NO_DOCUMENT_PAGE)
                   .forward(request, response);
            return;
        }

        request.setAttribute(Config.ATTRIBUTES.DOCUMENT_TEXT, 
                             document.getText()); 
        request.getRequestDispatcher(Config.PAGES.VIEW_PAGE)
               .forward(request, response);
    }

    @Override
    protected void doPost(HttpServletRequest request,
                          HttpServletResponse response)
            throws ServletException, IOException {
        try (PrintWriter out = response.getWriter()) {
            out.println("This servlet is not accessible via POST method.");
        }
    }
}

Document.java

package net.coderodde.roddenotes.model;

/**
 * This class implements a document.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Dec 15, 2017)
 */
public final class Document {

    private String id;
    private String editToken;
    private String text;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getEditToken() {
        return editToken;
    }

    public void setEditToken(String editToken) {
        this.editToken = editToken;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }
}

DataAccessObject.java

package net.coderodde.roddenotes.sql;

import java.sql.SQLException;
import net.coderodde.roddenotes.model.Document;

/**
 * This interface lists all the methods a data access object should implement in
 * order to integrate with rodde-notes.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Dec 15, 2017)
 */
public interface DataAccessObject {

    /**
     * Creates a new document with unique ID, random edit token and empty text.
     * 
     * @return a document.
     * @throws SQLException if the SQL layer fails.
     */
    public Document createNewDocument() throws SQLException;

    /**
     * Deletes a document with given ID.
     * 
     * @param id the ID of the document to delete.
     * @throws SQLException if the SQL layer fails.
     */
    public void deleteDocument(String id) throws SQLException;

    /**
     * Reads a document with given ID.
     * 
     * @param id the ID of the desired document.
     * @return the document with the given ID or {@code null} if there is no 
     *         such.
     * @throws SQLException if the SQL layer fails.
     */
    public Document getDocument(String id) throws SQLException;

    /**
     * Saves the document. If the document is not yet present in the database,
     * it is inserted. Otherwise, its state is updated.
     * 
     * @param document the document to update.
     * @return {@code true} if the ID and editToken match each other. 
     *         {@code false} otherwise.
     * @throws SQLException if the SQL layer fails.
     */
    public boolean updateDocument(Document document) throws SQLException;

    /**
     * Makes sure all the tables are created in the database.
     * 
     * @throws SQLException if the SQL layer fails.
     */
    public void initializeDatabaseTables() throws SQLException;
}

MySQLDataAccessObject.java

package net.coderodde.roddenotes.sql.support;

import java.net.URI;
import java.net.URISyntaxException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import net.coderodde.roddenotes.model.Document;
import net.coderodde.roddenotes.sql.DataAccessObject;
import net.coderodde.roddenotes.sql.support.MySQLDefinitions.DELETE;
import net.coderodde.roddenotes.sql.support.MySQLDefinitions.DOCUMENT_TABLE;
import net.coderodde.roddenotes.sql.support.MySQLDefinitions.SELECT;
import net.coderodde.roddenotes.sql.support.MySQLDefinitions.UPDATE;
import net.coderodde.roddenotes.util.RandomUtilities;

/**
 * This class implements a data access object over a MySQL database.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Dec 15, 2017)
 */
public final class MySQLDataAccessObject implements DataAccessObject {

    /**
     * The name of the environment variable holding the connection URI for the 
     * MySQL database server.
     */
    private static final String DATABASE_URI_ENVIRONMENT_VARIABLE = 
            "RODDE_NOTES_DB_URI";

    /**
     * The only instance of this class.
     */
    public static final MySQLDataAccessObject INSTANCE =
            new MySQLDataAccessObject();

    static {
        try {
            // Attempts to load the driver.
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException ex) {
            throw new RuntimeException("Cannot load the JDBC driver for MySQL.",
                                       ex);
        }
    }

    private MySQLDataAccessObject() {}

    /**
     * {@inheritDoc } 
     */
    @Override
    public Document createNewDocument() throws SQLException {
        String id = null;
        String editToken = RandomUtilities.generateRandomEditToken();

        try (Connection connection = getConnection()) {
            connection.setAutoCommit(false);

            try (PreparedStatement statement = 
                    connection.prepareStatement(MySQLDefinitions.SELECT.DOCUMENT.VIA_DOCUMENT_ID)) {

                while (true) {
                    id = RandomUtilities.generateRandomDocumentId();
                    statement.setString(1, id);

                    try (ResultSet resultSet = statement.executeQuery()) {
                        if (!resultSet.next()) {
                            break;
                        }
                    }
                }

            }

            try (PreparedStatement statement = 
                    connection.prepareStatement(
                            MySQLDefinitions.INSERT.DOCUMENT)) {

                statement.setString(1, id);
                statement.setString(2, editToken);
                statement.setString(3, ""); // Note the empty text.

                statement.executeUpdate();
            }

            connection.commit();
        }

        Document document = new Document();
        document.setId(id);
        document.setEditToken(editToken);
        document.setText(""); // Note the empty text.
        return document;
    }

    /**
     * {@inheritDoc } 
     */
    @Override
    public Document getDocument(String id) throws SQLException {
        try (Connection connection = getConnection()) {
            try (PreparedStatement statement = 
                    connection.prepareStatement(MySQLDefinitions
                                    .SELECT
                                    .DOCUMENT
                                    .VIA_DOCUMENT_ID)) {
                statement.setString(1, id);

                try (ResultSet resultSet = statement.executeQuery()) {
                    if (!resultSet.next()) {
                        return null;
                    }

                    Document document = new Document();
                    document.setId(
                            resultSet.getString(DOCUMENT_TABLE.ID_COLUMN.NAME));

                    document.setEditToken(
                            resultSet.getString(
                                    DOCUMENT_TABLE.EDIT_TOKEN_COLUMN.NAME));

                    document.setText(
                            resultSet.getString(
                                    DOCUMENT_TABLE.TEXT_COLUMN.NAME));

                    return document;
                }
            }
        }
    }

    private Connection getConnection() throws SQLException {
        URI dbUri = null;

        try {
            dbUri = new URI(System.getenv(DATABASE_URI_ENVIRONMENT_VARIABLE));
        } catch (URISyntaxException ex) {
            throw new RuntimeException("Bad URI syntax.", ex);
        }

        String[] tokens = dbUri.getUserInfo().split(":");
        String username = tokens[0];
        String password = tokens[1];
        String dbUrl = "jdbc:mysql://" + dbUri.getHost() + dbUri.getPath();
        return DriverManager.getConnection(dbUrl, username, password);
    }

    /**
     * {@inheritDoc } 
     */
    @Override
    public void initializeDatabaseTables() throws SQLException {
        try (Connection connection = getConnection()) {
            try (Statement statement = connection.createStatement()) {
                statement.executeUpdate(DOCUMENT_TABLE.CREATE_STATEMENT);
            }
        }
    }

    /**
     * {@inheritDoc } 
     */
    @Override
    public boolean updateDocument(Document document) throws SQLException {
        try (Connection connection = getConnection()) {
            connection.setAutoCommit(false);

            try (PreparedStatement statement = 
                    connection.prepareStatement(
                            SELECT.DOCUMENT.VIA_DOCUMENT_ID)) {
                statement.setString(1, document.getId());

                try (ResultSet resultSet = statement.executeQuery()) {
                   if (!resultSet.next()) {
                       return false;
                   }

                   String editToken = 
                           resultSet.getString(
                                   DOCUMENT_TABLE.EDIT_TOKEN_COLUMN.NAME);

                   if (!editToken.equals(document.getEditToken())) {
                       return false;
                   }
                }
            }

            try (PreparedStatement statement = 
                    connection.prepareStatement(UPDATE.DOCUMENT.VIA_DOCUMENT_ID)) {
                statement.setString(1, document.getText());
                statement.setString(2, document.getId());
                statement.executeUpdate();
            }

            connection.commit();
        }

        return true;
    }

    @Override
    public void deleteDocument(String id) throws SQLException {
        try (Connection connection = getConnection()) {
            try (PreparedStatement statement = 
                    connection.prepareStatement(DELETE.DOCUMENT)) {
                statement.setString(1, id);
                statement.executeUpdate();
            }
        }
    }
}

MySQLDefinitions.java

package net.coderodde.roddenotes.sql.support;

/**
 * This class defines all the data regarding the database schema for the 
 * rodde-notes app.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Dec 15, 2017)
 */
public final class MySQLDefinitions {

    /**
     * Defines the structure of the database table holding the note entries.
     */
    public static final class DOCUMENT_TABLE {

        /**
         * The name of the notes table.
         */
        public static final String TABLE_NAME = "rodde_notes_documents";

        /**
         * Describes the note ID column.
         */
        public static final class ID_COLUMN {

            /**
             * The name of the note ID column.
             */
            public static final String NAME = "document_id";

            /**
             * The length of IDs in characters.
             */
            public static final int LENGTH = 10;

            /**
             * The data type of the column.
             */
            public static final String TYPE = 
                    "CHAR(" + LENGTH + ") NOT NULL";
        }

        /**
         * Describes the edit token column.
         */
        public static final class EDIT_TOKEN_COLUMN {

            /**
             * The name of the edit token column.
             */
            public static final String NAME = "edit_token";

            /**
             * The length of edit tokens in characters.
             */
            public static final int LENGTH = 12;

            /**
             * The data type of the column.
             */
            public static final String TYPE = "CHAR(" + LENGTH + ") NOT NULL";
        }

        /**
         * Describes the text column.
         */
        public static final class TEXT_COLUMN {

            /**
             * The name of the text column.
             */
            public static final String NAME = "text";

            /**
             * The data type of the column.
             */
            public static final String TYPE = "TEXT NOT NULL";
        }

        /**
         * The SQL statement for creating the note table.
         */
        public static final String CREATE_STATEMENT =
                "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (\n" +
                "  " + ID_COLUMN.NAME + " " + ID_COLUMN.TYPE + ",\n" +
                "  " + EDIT_TOKEN_COLUMN.NAME + " " + 
                       EDIT_TOKEN_COLUMN.TYPE + ",\n" +
                "  " + TEXT_COLUMN.NAME + " " + TEXT_COLUMN.TYPE + ",\n" +
                "  PRIMARY KEY(" + ID_COLUMN.NAME + "));";
    }

    /**
     * Contains all the delete statements.
     */
    public static final class DELETE {

        /**
         * Deletes the document from the database.
         */
        public static final String DOCUMENT = 
                "DELETE FROM " + DOCUMENT_TABLE.TABLE_NAME + " WHERE " +
                DOCUMENT_TABLE.ID_COLUMN.NAME + " = ?;";
    }

    /**
     * Contains all the insert statements.
     */
    public static final class INSERT {

        /**
         * Inserts a document.
         */
        public static final String DOCUMENT = 
                "INSERT INTO " + DOCUMENT_TABLE.TABLE_NAME + 
                " VALUES (?, ?, ?);";
    }

    /**
     * Contains all the select statements.
     */
    public static final class SELECT {

        /**
         * Contains all the select statements selecting documents.
         */
        public static final class DOCUMENT {

            /**
             * Selects a document via an ID.
             */
            public static final String VIA_DOCUMENT_ID = 
                    "SELECT * FROM " + DOCUMENT_TABLE.TABLE_NAME + " WHERE " +
                    DOCUMENT_TABLE.ID_COLUMN.NAME + " = ?;";

            /**
             * Selects a document via an ID and an edit token.
             */
            public static final String VIA_DOCUMENT_ID_AND_EDIT_TOKEN = 
                    "SELECT * FROM " + DOCUMENT_TABLE.TABLE_NAME + " WHERE " +
                    DOCUMENT_TABLE.ID_COLUMN.NAME + " = ? AND " +
                    DOCUMENT_TABLE.EDIT_TOKEN_COLUMN.NAME + " = ?;";
        }
    }

    /**
     * Contains all the update statements.
     */
    public static final class UPDATE {

        /**
         * Contains all the update statements on the document.
         */
        public static final class DOCUMENT {

            /**
             * Updates the text via the document ID.
             */
            public static final String VIA_DOCUMENT_ID = 
                    "UPDATE " + DOCUMENT_TABLE.TABLE_NAME + " SET " +
                    DOCUMENT_TABLE.TEXT_COLUMN.NAME + " = ? WHERE " +
                    DOCUMENT_TABLE.ID_COLUMN.NAME + " = ?;";
            }
    }
}

MiscellaneousUtilities.java

package net.coderodde.roddenotes.util;

import javax.servlet.http.HttpServletRequest;

/**
 * This class provides various facilities.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Dec 15, 2017)
 */
public final class MiscellaneousUtilities {

    /**
     * Returns the full URL of the web application.
     * 
     * @param request the servlet request object.
     * @return an URL.
     */
    public static String getServerURL(HttpServletRequest request) {
        String url = request.getRequestURL().toString();
        int lastSlashIndex = url.lastIndexOf('/');
        return url.substring(0, lastSlashIndex);
    }
}

RandomUtilities.java

package net.coderodde.roddenotes.util;

import java.util.concurrent.ThreadLocalRandom;
import net.coderodde.roddenotes.sql.support.MySQLDefinitions;

/**
 * This class provides various utilities for dealing with random strings.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Dec 15, 2017)
 */
public final class RandomUtilities {

    private static final char[] ALPHABET = new char[62];

    static {
        int index = 0;

        for (char c = '0'; c <= '9'; ++c) {
            ALPHABET[index++] = c;
        }

        for (char c = 'A'; c <= 'Z'; ++c) {
            ALPHABET[index++] = c;
        }

        for (char c = 'a'; c <= 'z'; ++c) {
            ALPHABET[index++] = c;
        }
    }

    /**
     * Generates a random document ID.
     * 
     * @return a document ID.
     */
    public static String generateRandomDocumentId() {
        return generateRandomString(
                MySQLDefinitions.DOCUMENT_TABLE.ID_COLUMN.LENGTH);
    }

    /**
     * Generates a random edit token.
     * 
     * @return an edit token.
     */
    public static String generateRandomEditToken() {
        return generateRandomString(
                MySQLDefinitions.DOCUMENT_TABLE.EDIT_TOKEN_COLUMN.LENGTH);
    }

    /**
     * Generates a random string of given length.
     * 
     * @param length the length of the string to generate.
     * @return a random string.
     */
    public static String generateRandomString(int length) {
        ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
        StringBuilder stringBuilder = new StringBuilder(length);

        for (int i = 0; i < length; ++i) {
            stringBuilder.append(
                    ALPHABET[threadLocalRandom.nextInt(ALPHABET.length)]);
        }

        return stringBuilder.toString();
    }
}

RoddenoteServletContextListener.java

package net.coderodde.roddenotes.util;

import java.sql.SQLException;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import net.coderodde.roddenotes.sql.support.MySQLDataAccessObject;

/**
 * This class implements a servlet context listener.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Dec 15, 2017)
 */
public final class RoddenoteServletContextListener 
        implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent sce) {
        try {
            MySQLDataAccessObject.INSTANCE.initializeDatabaseTables();
        } catch (SQLException ex) {
            throw new RuntimeException(ex);
        }
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {

    }
}

Code (front-end)

code.js

var RoddeNotes = {};

RoddeNotes.Parameters = {};
RoddeNotes.Parameters.DOCUMENT_ID = "documentId";
RoddeNotes.Parameters.EDIT_TOKEN = "editToken";
RoddeNotes.Parameters.EDITOR_TEXT_AREA = "editorTextArea";

function moveTextToDocument() {
    var editorElement = 
            document.getElementById(
            RoddeNotes.Parameters.EDITOR_TEXT_AREA);

    var documentViewElement = 
            document.getElementById("documentView");

    var documentText = editorElement.value;
    documentViewElement.innerHTML = documentText;
}

function typeset() {
    MathJax.Hub.Queue(["Typeset", MathJax.Hub]);
}

function startTypesettingLoop() {
    setInterval(function() {typeset();}, 5000);
}

function startSaveLoop() {
    setInterval(function() {save();}, 10000);
}

function save() {
    var documentId = 
            document.getElementById(
            RoddeNotes.Parameters.DOCUMENT_ID).value;

    var editToken =
            document.getElementById(
            RoddeNotes.Parameters.EDIT_TOKEN).value;

    var documentText = 
            document.getElementById(
            RoddeNotes.Parameters.EDITOR_TEXT_AREA).value;
    documentText = encodeURIComponent(documentText);
    console.log(documentText);

    var xhr = new XMLHttpRequest();

    xhr.onreadystatechange = function() {
        if (this.readyState === 4 && this.status === 200) {
            var response = this.responseText;

            if (response == "success") {
                flashStatusSuccessMessage();
            } else if (response == "failure") {
                flashStatusFailureMessage();
            }
        }
    };

    xhr.open("POST", "update", true);
    xhr.setRequestHeader("Content-type", 
                         "application/x-www-form-urlencoded");
    xhr.send("documentId=" + documentId +
             "&editToken=" + editToken + 
             "&documentText=" + documentText);
}

function flashStatusSuccessMessage() {
    $("#savedSuccessful").fadeIn();
    setTimeout(function() {
        $("#savedSuccessful").fadeOut();
    }, 1500);
}

function flashStatusFailureMessage() {
    $("#savedFailed").fadeIn();
    setTimeout(function() {
        $("#savedFailed").fadeOut();
    }, 1500);
}

function deleteDocument() {
    var input = prompt("Confirm current document ID:");

    var documentId = 
            document.getElementById(
            RoddeNotes.Parameters.DOCUMENT_ID).value;

    var editToken =
            document.getElementById(
            RoddeNotes.Parameters.EDIT_TOKEN).value;

    if (documentId != input) {
        return;
    }

    var xhr = new XMLHttpRequest();

    xhr.onreadystatechange = function() {
        if (this.readyState === 4 && this.status === 200) {
            var response = this.responseText;

            if (response == "success") {
                window.location = "view?documentId=" + documentId;
            }
        }
    };

    xhr.open("POST", "deleteDocument", true);
    xhr.setRequestHeader("Content-type", 
                         "application/x-www-form-urlencoded");
    xhr.send("documentId=" + documentId + "&editToken=" + editToken);
}

styles.css

<%-- We indent this code so that when it ends up in a         
response HTML document
 the document seems properly indented. --%>
        #page {
            width: 800px;  
            margin: auto;
        }  

        .topNotifications {
            width: 800px;
            text-align: center;
            vertical-align: central;
            border-width: 2px;
            border-style: solid;
            margin: 0;
            padding-top: 15px;
            padding-bottom: 15px;
            font-family: sans-serif;
            font-size: 15px;
            margin-bottom: 1px;
            display: none;
        }

        #savedSuccessful {
            border-color: darkgreen;
            background-color: lightgreen;
            color: darkgreen;
        }

        #savedFailed {
            border-color: red;
            background-color: pink;
            color: red;
        }

        #documentContainer {
            width: 100%;
        }

        #documentView {
            width: 100%;
        }

        #editorTextArea {
            width: 100%;
            border-width: 2px;
            height: 200px;
            margin: 0;
            padding: 0;
            resize: none;
            font-family: monospace;
            font-size: 11pt;
        }

        .button {
            width: 100%;
            border-width: 2px;
            height: 30px;
            font-family: sans-serif;
            font-size: 15px;
        }

        #publishLink {
            width: 100%;
            border: 2px solid blue;
            background-color: lightblue;
            color: blue;
            margin: 0;
            margin-top: 3px;
            padding-top: 15px;
            padding-bottom: 15px;
            padding-left: 10px;
            padding-right: 10px;
            font-family: sans-serif;
            font-size: 15px;
            display: inline-block;
            box-sizing: border-box;
              -moz-box-sizing: border-box;
              -webkit-box-sizing: border-box;
            text-align: left;
            overflow: hidden;
        }

        #publishLinkLabel {
            font-family: sans-serif;
            font-size: 15px;
            padding-right: 15px;
            padding-bottom: 7px;    
        }

        #publishLinkContent {
            background-color: white;
            color: gray;
            font-family: monospace;
            font-size: 20px;
            padding: 3px;
            padding-left: 5px;
            padding-right: 5px;
        }

        .error {
            color: red;
        }

edit.jsp

<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>rodde-notes</title>
        <style>
            <%@include file="styles.css" %>
        </style>
        <script
            src="https://code.jquery.com/jquery-3.2.1.min.js"
            integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
            crossorigin="anonymous">

        </script>
        <script
            src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"
            integrity="sha256-VazP97ZCwtekAsvgPBSUwPFKdrwD3unUfSGVYrahUqU="
            crossorigin="anonymous">

        </script>

        <script src='https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.2/MathJax.js?config=TeX-MML-AM_CHTML'></script>

        <script type="text/x-mathjax-config">
            MathJax.Hub.Config({tex2jax: {inlineMath: [['$','$'], ['\\[','\\]']]}});
        </script>

        <script>
            <%@include file="code.js" %>
        </script>
    </head>

    <body>
        <div id="page">
            <div id="documentContainer">
                <div id="documentView" align="justify"></div>
                <textarea id="editorTextArea" oninput="moveTextToDocument()">${documentText}</textarea>
                <button id="typesetButton" class="button" onclick="typeset()">Typeset!</button>
                <button id="saveButton" class="button" onclick="save()">Save!</button>
                <button id="deleteButton" class="button" onclick="deleteDocument()">Delete!</button>
                <div id="publishLink">
                    <div id="publishLinkLabel">Non-editable publish link:</div>
                    <div id="publishLinkContent">${publishLink}</div>
                </div>
            </div>

            <form id="dataForm">
                <input type="hidden" value="${documentId}" id="documentId" />
                <input type="hidden" value="${editToken}" id="editToken" />
            </form>

            <form id="deleteForm" action="delete" method="post" style="display: none;">
                <input type="text" id="idField" name="documentId"/>
                <button type="submit">Delete</button>
            </form>
            <div id ="savedSuccessful" class="topNotifications">
                The document is updated.
            </div>

            <div id="savedFailed" class="topNotifications">
                Could not update the document.
            </div>
        </div>

        <script>
            startTypesettingLoop();
            startSaveLoop();
            moveTextToDocument();
        </script>
    </body>
</html>

view.jsp

<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>rodde-notes</title>
        <style>
            #view {
                width: 800px;
                margin: auto;
            }

            #text {
                text-align: justify;
            }
        </style>

        <script src='https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.2/MathJax.js?config=TeX-MML-AM_CHTML'></script>

        <script type="text/x-mathjax-config">
            MathJax.Hub.Config({tex2jax: {inlineMath: [['$','$'], ['\\[','\\]']]}});
        </script>
    </head>
    <body>
        <div id="view">
            <p id="text">${documentText}</p>
        </div>
    </body>
</html>

(The entire project lives here.)

Critique request

I would primarily like to hear comments regarding these aspects:

  1. JavaScript coding conventions
  2. CSS coding conventions
  3. Java coding conventions
  4. URL "design"
  5. Usability
\$\endgroup\$
1
\$\begingroup\$

What I found in your code (just the Java and the Javascript part) is the complexity mix inside methods / functions, and the strong coupling of objects.

Complexity mixin

This happens when you write code that handle with different level of abstraction.

The problem is that it is very difficult to follow the code as you jump from high level of absraction to a much low level details.

Another problem is that such methods / functions use a lot of data, objects or in general dependencies, and more bad they couple together those dependencies.

In order to have good code maintenance we should avoid coupling.

Let me provide 2 examples here, first your servlet:

@WebServlet(name = "DeleteDocumentServlet", urlPatterns = {"/deleteDocument"})
public class DeleteDocumentServlet extends HttpServlet {

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

            ...

            if (documentId == null || editToken == null) {
                out.print(Config.STATUS_MESSAGES.FAILURE);
                return;
            }

            Document document = null;

            try {
                document = MySQLDataAccessObject.INSTANCE.getDocument(documentId);
            } catch (SQLException ex) {
                out.print(Config.STATUS_MESSAGES.FAILURE);
                throw new RuntimeException(ex);
            }

            ...

This 2 block of code inside your servlet, show exactly the poin. In a servlet method you should work on the response to a request. So you do in the first if statement, where you build the response and write it.

But the try catch part, access to a DB! A servlet should not access to a DB or other resources directly. Instead you should create a new class that handle the DB operations and that have an interface API that the servlet could use. This object should not be a singleton, but just a servlet parameter.

So you should have something like this:

private Store store = null;
...
public DeleteDocumentServlet(Store store) {
    this.store = store;
}

...
document = store.getDocumentFromId(documentId);

The advantange is you don't have to know about your DB layer in the servlet. You just have an API (Store should be an interface) to use and nothing more. So you decouple your servlet from the other part of your system, and even the code will be much easy to follow and change.

Another point in this servlet:

        try {
            MySQLDataAccessObject.INSTANCE.deleteDocument(documentId);
            out.print(Config.STATUS_MESSAGES.SUCCESS);
        } catch (SQLException ex) {
            throw new RuntimeException(ex);
        }

In the other cases you handle the exception well providing an handled error message. But here you just throw an exception.

I think you should use a logger to trace the errors here and just provide an error response.

Now the Javascript example:

function save() {
    var documentId = 
            document.getElementById(
            RoddeNotes.Parameters.DOCUMENT_ID).value;

    var editToken =
            document.getElementById(
            RoddeNotes.Parameters.EDIT_TOKEN).value;

    var documentText = 
            document.getElementById(
            RoddeNotes.Parameters.EDITOR_TEXT_AREA).value;
    documentText = encodeURIComponent(documentText);
    console.log(documentText);

    var xhr = new XMLHttpRequest();

    xhr.onreadystatechange = function() {
        if (this.readyState === 4 && this.status === 200) {
            var response = this.responseText;

            if (response == "success") {
                flashStatusSuccessMessage();
            } else if (response == "failure") {
                flashStatusFailureMessage();
            }
        }
    };

    xhr.open("POST", "update", true);
    xhr.setRequestHeader("Content-type", 
                         "application/x-www-form-urlencoded");
    xhr.send("documentId=" + documentId +
             "&editToken=" + editToken + 
             "&documentText=" + documentText);
}

As you can see here you mix different levels of complexity together. How you package the message / object to be saved on the server has nothing to do with how it is delivered to the server.

You should wrap the XMLHttpRequest in a function and just use such function to do this task. The advantage is in reusability, as you have more functions that need to speak with the server.

Another point about the advantage to have a separate function is the problem of cross browser. If you fall in such problem, the changes could be a hell if you have to read all your code and copy 'n paste the changes everywhere. And then you should test each part as of your code...

So you can have a send function that provide this feature, or better use a much standard fetch polyfill library in order to abstract this part.

And then I saw this two things:

...
var documentId = 
        document.getElementById(
        RoddeNotes.Parameters.DOCUMENT_ID).value;
...
$("#savedSuccessful").fadeIn();

Are you using jQuery? If so, why use the DOM API! If you have the jQuery lib just use that lib to handle with the DOM. And about the client/server comunication, with jQuery you have the $.ajax function that do it for you, so try to stay on the proper abstraction level.

Strong coupling of objects

Have a strong coupling is always a bad idea. Each object should be isolated as much as possibile from the rest of the system.

It is not only about reausability in other projects or something like this.

The strong coupling is a huge issue when you have to introduce changes in your project, as you start to soffer strange side effects and errors.

Each object / class should be designed to have a public API, that should be the smallest possible.

I saw you use singletons to perform system coinfiguration. As your project is very small, why not considering to put the needed infos in the constructor?

For the servlet, you can configure the parameters for the constructor in the xml file web.xml. That is a perfect place where to put configuration. In such way you could have different configuration without recompile the application.

You can do this with your Javascript code too.

I noticed thay your Javascript code is not wrapped inside a function. You should do this in order to avoid to have global variables or conflicts with different libraries or components.

So just do:

// here you define the list of dependencies with the name 
// you're going to use.
(function RoddeNotes($, documentId, documentView, editToken, textArea) {
     // As you are in a function you can store your 
     // jQuery search in variables that have a function 
     // scope.
     var editorElement = $(editorTextArea);
     var documentViewElement = $(documentView);
     var documentIdElement = $(documentId);
     var editTokenElement = $(editToken);

     function moveTextToDocument() {
         documentViewElement.html(documentElement.text());
     }

     function typeset() {
         MathJax.Hub.Queue(["Typeset", MathJax.Hub]);
     }

     function startTypesettingLoop() {
         setInterval(function() {typeset();}, 5000);
     }

     function startSaveLoop() {
         setInterval(function() {save();}, 10000);
     }

     function save() {
         var payload = {
             "documentId": documentId.val(),
             "editToken": editToken.val(), 
             "documentText": documentText.val()
         };

         $.ajax({
            type: "POST",
            url: "update",
            data: payload,
            success: /* function on success... */,
            dataType: "text"
         });
    }

    function flashStatusSuccessMessage() {
        $("#savedSuccessful").fadeIn();
        setTimeout(function() {
            $("#savedSuccessful").fadeOut();
        }, 1500);
    }

    function flashStatusFailureMessage() {
        $("#savedFailed").fadeIn();
        setTimeout(function() {
            $("#savedFailed").fadeOut();
        }, 1500);
    }

    function deleteDocument() {
        var input = prompt("Confirm current document ID: " + documentId.val());

        if (!input) return;

        var payload = {
             "documentId": documentId.val(),
             "editToken": editToken.val(), 
             "documentText": documentText.val()
         };

         $.ajax({
            type: "POST",
            url: "deleteDocument",
            data: payload,
            success: /* function on success... */,
            dataType: "text"
         });
     }

     // if you need to use the functions outside, then create an object:
     var RN = {
          "startTypesettingLoop": startTypesettingLoop,
          "startSaveLoop": startSaveLoop,
          // so on...
     };

     // and you return this object that will be your module API.
     return RN;
// here is where you pass the parameters values.
})(jQuery, "documentId", "documentView", "editToken", "editorTextArea"); 

Sorry I just answered on just 1 point of your list, but there are several thing to say and this is just goes too long. Hope others could address the missed points.

\$\endgroup\$
  • \$\begingroup\$ Very good points, thanks! A question, where in my web app should I set the dependencies (such a DataStore object for the servlets)? \$\endgroup\$ – coderodde Dec 17 '17 at 14:31
  • 1
    \$\begingroup\$ In web.xml where you define the <servlet> you could provide <init-param>. For DataStore you could use jndi, following the web container documentation. Here an example with Tomcat. Once you have that you could inject in your servlet. \$\endgroup\$ – Mario Santini Dec 17 '17 at 16:04
  • \$\begingroup\$ I understood that getInitParameter() returns only a String. How should I go about instantiating the servlets with a class implementing a data store interface? \$\endgroup\$ – coderodde Dec 17 '17 at 18:59
  • \$\begingroup\$ Would it be good practice to call getInitParameter("myDAOparam") in Servlet.init() and switch on that string plugging in the desired DAO implementation? \$\endgroup\$ – coderodde Dec 17 '17 at 19:07
  • 1
    \$\begingroup\$ Once you defined a JNDI resource you can use dependecy injection in your servlet. There are several ways to do that, I can just point you to a tutoral that show one of those: theopentutorials.com/tutorials/java-ee/servlet/…. The point I raised is that you should inject the DAO in a class that is not the servlet. \$\endgroup\$ – Mario Santini Dec 17 '17 at 19:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.