I have this web application for sharing temporary notes. It supports HTML markup (<p>
, <code>
, etc.) along the TeX code.
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 = "<script>";
private static final String SCRIPT_TAG_END_SUBSTITUTE = "</script>";
@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:
- JavaScript coding conventions
- CSS coding conventions
- Java coding conventions
- URL "design"
- Usability