· 8 years ago · Dec 16, 2017, 05:36 PM
1package net.coderodde.roddenotes.config;
2
3/**
4 * This class contains the general application parameters.
5 *
6 * @author Rodion "rodde" Efremov
7 * @version 1.6 (Dec 15, 2017)
8 */
9public final class Config {
10
11 /**
12 * This class contains form parameter names.
13 */
14 public static final class PARAMETERS {
15
16 /**
17 * The name of the parameter holding the document ID.
18 */
19 public static final String DOCUMENT_ID = "documentId";
20
21 /**
22 * The name of the parameter holding the edit token.
23 */
24 public static final String EDIT_TOKEN = "editToken";
25
26 /**
27 * The name of the parameter holding the document text.
28 */
29 public static final String DOCUMENT_TEXT = "documentText";
30 }
31
32 /**
33 * This class contains attribute names.
34 */
35 public static final class ATTRIBUTES {
36
37 /**
38 * Used for communicating the actual document text.
39 */
40 public static final String DOCUMENT_TEXT = "documentText";
41
42 /**
43 * Used for communicating publish links.
44 */
45 public static final String PUBLISH_LINK = "publishLink";
46
47 /**
48 * Used for communicating document IDs.
49 */
50 public static final String DOCUMENT_ID = "documentId";
51
52 /**
53 * Used for communicating edit tokens.
54 */
55 public static final String EDIT_TOKEN = "editToken";
56 }
57
58 public static final class PAGES {
59
60 /**
61 * The name of the JSP file for viewing a (non-editable) document.
62 */
63 public static final String VIEW_PAGE = "view.jsp";
64
65 /**
66 * The name of the JSP file for editing documents.
67 */
68 public static final String EDITOR_PAGE = "edit.jsp";
69
70 /**
71 * The name of the JSP file rendered upon missing document.
72 */
73 public static final String NO_DOCUMENT_PAGE = "viewDocNotFound.jsp";
74
75 /**
76 * The name of the HTML file rendered upon requesting a view without the
77 * document ID parameter.
78 */
79 public static final String NO_ID_VIEW_PAGE = "viewIdNotGiven.html";
80
81 /**
82 * The name of the HTML file rendered whenever receiving a request where
83 * the document ID and the edit token do not match.
84 */
85 public static final String DONT_HACK_US_PAGE = "dontHackUs.html";
86 }
87
88 /**
89 * This class contains all the error messages in the application.
90 */
91 public static final class ERROR_MESSAGES {
92
93 /**
94 * The name of the CSS class used for rendering error messages.
95 */
96 public static final String ERROR_MESSAGE_CSS_CLASS = "error";
97
98 /**
99 * The opening span tag.
100 */
101 public static final String SPAN_BEGIN =
102 "<span class='" +
103 ERROR_MESSAGE_CSS_CLASS +
104 "'>";
105
106 /**
107 * The closing span tag.
108 */
109 public static final String SPAN_END = "</span>";
110
111 /**
112 * The text rendered whenever the document with given ID does not exist.
113 */
114 public static final String NO_SUCH_DOCUMENT_TEXT_FORMAT =
115 SPAN_BEGIN + "(Document with ID %s does not exist.)" + SPAN_END;
116
117 /**
118 * The text rendered whenever the user accesses the view page without a any
119 * document ID.
120 */
121 public static final String NO_GIVEN_ID_TEXT =
122 SPAN_BEGIN +
123 "(Cannot find a document without an ID.)" +
124 SPAN_END;
125 }
126
127 public static final class STATUS_MESSAGES {
128
129 public static final String SUCCESS = "success";
130 public static final String FAILURE = "failure";
131 }
132}
133
134package net.coderodde.roddenotes.controllers;
135
136import java.io.IOException;
137import java.io.PrintWriter;
138import java.sql.SQLException;
139import javax.servlet.ServletException;
140import javax.servlet.annotation.WebServlet;
141import javax.servlet.http.HttpServlet;
142import javax.servlet.http.HttpServletRequest;
143import javax.servlet.http.HttpServletResponse;
144import net.coderodde.roddenotes.config.Config;
145import net.coderodde.roddenotes.model.Document;
146import net.coderodde.roddenotes.sql.support.MySQLDataAccessObject;
147
148/**
149 * This servlet is responsible for deleting documents from the database.
150 *
151 * @author Rodion "rodde" Efremov
152 * @version 1.6 (Dec 16, 2017)
153 */
154@WebServlet(name = "DeleteDocumentServlet", urlPatterns = {"/deleteDocument"})
155public class DeleteDocumentServlet extends HttpServlet {
156
157 @Override
158 protected void doPost(HttpServletRequest request,
159 HttpServletResponse response)
160 throws ServletException, IOException {
161 try (PrintWriter out = response.getWriter()) {
162 String documentId =
163 request.getParameter(Config.PARAMETERS.DOCUMENT_ID);
164
165 String editToken =
166 request.getParameter(Config.PARAMETERS.EDIT_TOKEN);
167
168 if (documentId == null || editToken == null) {
169 out.print(Config.STATUS_MESSAGES.FAILURE);
170 return;
171 }
172
173 Document document = null;
174
175 try {
176 document = MySQLDataAccessObject.INSTANCE.getDocument(documentId);
177 } catch (SQLException ex) {
178 out.print(Config.STATUS_MESSAGES.FAILURE);
179 throw new RuntimeException(ex);
180 }
181
182 if (document == null) {
183 out.print(Config.STATUS_MESSAGES.FAILURE);
184 return;
185 }
186
187 if (!document.getEditToken().equals(editToken)) {
188 out.print(Config.STATUS_MESSAGES.FAILURE);
189 return;
190 }
191
192 try {
193 MySQLDataAccessObject.INSTANCE.deleteDocument(documentId);
194 out.print(Config.STATUS_MESSAGES.SUCCESS);
195 } catch (SQLException ex) {
196 throw new RuntimeException(ex);
197 }
198 }
199 }
200}
201
202package net.coderodde.roddenotes.controllers;
203
204import java.io.IOException;
205import java.io.PrintWriter;
206import java.sql.SQLException;
207import javax.servlet.ServletException;
208import javax.servlet.annotation.WebServlet;
209import javax.servlet.http.HttpServlet;
210import javax.servlet.http.HttpServletRequest;
211import javax.servlet.http.HttpServletResponse;
212import net.coderodde.roddenotes.config.Config;
213import net.coderodde.roddenotes.config.Config.ATTRIBUTES;
214import net.coderodde.roddenotes.model.Document;
215import net.coderodde.roddenotes.sql.support.MySQLDataAccessObject;
216import static net.coderodde.roddenotes.util.MiscellaneousUtilities.getServerURL;
217
218/**
219 * This servlet handles the edit requests. If the servlet receives parameters
220 * defining the document ID and its edit token, and both are valid, this servlet
221 * prepares an editor view for the document. Otherwise, a new document is
222 * created and is presented to the user.
223 * @author Rodion "rodde" Efremov
224 * @version 1.6 (Dec 15, 2017)
225 */
226@WebServlet(name = "EditServlet", urlPatterns = {"/edit"})
227public class EditServlet extends HttpServlet {
228
229 @Override
230 protected void doGet(HttpServletRequest request,
231 HttpServletResponse response)
232 throws ServletException, IOException {
233 String id = request.getParameter(Config.PARAMETERS.DOCUMENT_ID);
234 String editToken = request.getParameter(Config.PARAMETERS.EDIT_TOKEN);
235
236 if (id == null || editToken == null) {
237 serveFreshEmptyDocument(request, response);
238 return;
239 }
240
241 Document document = null;
242
243 try {
244 document = MySQLDataAccessObject.INSTANCE.getDocument(id);
245 } catch (SQLException ex) {
246 throw new RuntimeException(ex);
247 }
248
249 if (document == null) {
250 serveFreshEmptyDocument(request, response);
251 return;
252 }
253
254 if (!document.getEditToken().equals(editToken)) {
255 serveFreshEmptyDocument(request, response);
256 return;
257 }
258
259 request.setAttribute(ATTRIBUTES.DOCUMENT_ID, document.getId());
260 request.setAttribute(ATTRIBUTES.EDIT_TOKEN, document.getEditToken());
261 request.setAttribute(ATTRIBUTES.DOCUMENT_TEXT, document.getText());
262 request.setAttribute(ATTRIBUTES.PUBLISH_LINK,
263 getPublishLink(request, document));
264
265 request.getRequestDispatcher(Config.PAGES.EDITOR_PAGE)
266 .forward(request, response);
267 }
268
269 @Override
270 protected void doPost(HttpServletRequest request,
271 HttpServletResponse response)
272 throws ServletException, IOException {
273 try (PrintWriter out = response.getWriter()) {
274 out.println("This servlet does not serve POST requests.");
275 }
276 }
277
278 private String getPublishLink(HttpServletRequest request,
279 Document document) {
280 return new StringBuilder(getServerURL(request))
281 .append("/view?")
282 .append(Config.PARAMETERS.DOCUMENT_ID)
283 .append('=')
284 .append(document.getId())
285 .toString();
286 }
287
288 private void serveFreshEmptyDocument(HttpServletRequest request,
289 HttpServletResponse response)
290 throws ServletException, IOException {
291 Document document = null;
292
293 try {
294 document = MySQLDataAccessObject.INSTANCE.createNewDocument();
295 } catch (SQLException ex) {
296 throw new RuntimeException(ex);
297 }
298
299 String path = getPath(request, document);
300 request.getRequestDispatcher(path).forward(request, response);
301 }
302
303 private String getPath(HttpServletRequest request, Document document) {
304 return new StringBuilder().append(request.getPathInfo())
305 .append('?')
306 .append(Config.PARAMETERS.DOCUMENT_ID)
307 .append('=')
308 .append(document.getId())
309 .append('&')
310 .append(Config.PARAMETERS.EDIT_TOKEN)
311 .append('=')
312 .append(document.getEditToken())
313 .toString();
314 }
315}
316
317package net.coderodde.roddenotes.controllers;
318
319import java.io.IOException;
320import java.io.PrintWriter;
321import java.sql.SQLException;
322import javax.servlet.ServletException;
323import javax.servlet.annotation.WebServlet;
324import javax.servlet.http.HttpServlet;
325import javax.servlet.http.HttpServletRequest;
326import javax.servlet.http.HttpServletResponse;
327import net.coderodde.roddenotes.config.Config;
328import net.coderodde.roddenotes.model.Document;
329import net.coderodde.roddenotes.sql.support.MySQLDataAccessObject;
330
331/**
332 * This servlet listens to the root resource of this application, creates a new
333 * document and redirects to the document's edit view.
334 *
335 * @author Rodion "rodde" Efremov
336 * @version 1.6 (Dec 15, 2017)
337 */
338@WebServlet(name = "HomeServlet", urlPatterns = {""})
339public class HomeServlet extends HttpServlet {
340
341 private static final String EDIT_SERVLET_NAME = "edit";
342
343 @Override
344 protected void doGet(HttpServletRequest request,
345 HttpServletResponse response)
346 throws ServletException, IOException {
347 Document document = null;
348
349 try {
350 document = MySQLDataAccessObject.INSTANCE.createNewDocument();
351 } catch (SQLException ex) {
352 throw new RuntimeException(ex);
353 }
354
355 if (document == null) {
356 throw new NullPointerException("Creating a document failed.");
357 }
358
359 response.sendRedirect(getEditPageAddress(document));
360 }
361
362 @Override
363 protected void doPost(HttpServletRequest request, HttpServletResponse response)
364 throws ServletException, IOException {
365 try (PrintWriter out = response.getWriter()) {
366 out.print("Please access this resource via GET method.");
367 }
368 }
369
370 /**
371 * Constructs the address for the edit page.
372 *
373 * @param document the document to prepare for editing.
374 * @return a page address relative to the web application.
375 */
376 private String getEditPageAddress(Document document) {
377 return new StringBuilder()
378 .append(EDIT_SERVLET_NAME)
379 .append('?')
380 .append(Config.PARAMETERS.DOCUMENT_ID)
381 .append('=')
382 .append(document.getId())
383 .append('&')
384 .append(Config.PARAMETERS.EDIT_TOKEN)
385 .append('=')
386 .append(document.getEditToken())
387 .toString();
388 }
389}
390
391package net.coderodde.roddenotes.controllers;
392
393import java.io.IOException;
394import java.io.PrintWriter;
395import java.sql.SQLException;
396import java.util.regex.Pattern;
397import javax.servlet.ServletException;
398import javax.servlet.annotation.WebServlet;
399import javax.servlet.http.HttpServlet;
400import javax.servlet.http.HttpServletRequest;
401import javax.servlet.http.HttpServletResponse;
402import net.coderodde.roddenotes.config.Config;
403import net.coderodde.roddenotes.config.Config.PAGES;
404import net.coderodde.roddenotes.config.Config.PARAMETERS;
405import net.coderodde.roddenotes.model.Document;
406import net.coderodde.roddenotes.sql.support.MySQLDataAccessObject;
407
408/**
409 * This servlet is responsible for updating an existing document. If the
410 * incoming document is not yet in the database, it is put there.
411 *
412 * @author Rodion "rodde" Efremov
413 * @version 1.6 (Dec 15, 2017)
414 */
415@WebServlet(name = "UpdateDocumentServlet", urlPatterns = {"/update"})
416public class UpdateDocumentServlet extends HttpServlet {
417
418 /**
419 * The regular expression for the begin script tag.
420 */
421 private static final String SCRIPT_TAG_BEGIN_REGEX = "<\s*script\s*>";
422
423 /**
424 * The regular expression for the end script tag.
425 */
426 private static final String SCRIPT_TAG_END_REGEX = "<\s*/\s*script\s*>";
427
428 private static final String SCRIPT_TAG_BEGIN_SUBSTITUTE = "<script>";
429 private static final String SCRIPT_TAG_END_SUBSTITUTE = "</script>";
430
431 @Override
432 protected void doGet(HttpServletRequest request,
433 HttpServletResponse response)
434 throws ServletException, IOException {
435 try (PrintWriter out = response.getWriter()) {
436 out.println("This servlet does not work via GET method.");
437 }
438 }
439
440 @Override
441 protected void doPost(HttpServletRequest request, HttpServletResponse response)
442 throws ServletException, IOException {
443 String documentId = request.getParameter(PARAMETERS.DOCUMENT_ID);
444 String documentText = request.getParameter(PARAMETERS.DOCUMENT_TEXT);
445 String editToken = request.getParameter(PARAMETERS.EDIT_TOKEN);
446
447 documentText = sanitizeText(documentText);
448
449 try (PrintWriter out = response.getWriter()) {
450 if (documentId == null
451 || editToken == null
452 || documentText == null) {
453 out.print(Config.STATUS_MESSAGES.FAILURE);
454 return;
455 }
456
457 Document document = new Document();
458 document.setId(documentId);
459 document.setEditToken(editToken);
460 document.setText(documentText);
461
462 try {
463 boolean validUpdate =
464 MySQLDataAccessObject.INSTANCE.updateDocument(document);
465
466 if (!validUpdate) {
467 request.getRequestDispatcher(PAGES.DONT_HACK_US_PAGE)
468 .forward(request, response);
469 return;
470 }
471
472 out.print(Config.STATUS_MESSAGES.SUCCESS);
473 } catch (SQLException ex) {
474 throw new RuntimeException(ex);
475 }
476 }
477 }
478
479 private String sanitizeText(String text) {
480 Pattern patternBeginTag =
481 Pattern.compile(SCRIPT_TAG_BEGIN_REGEX,
482 Pattern.CASE_INSENSITIVE);
483
484 Pattern patternEndTag =
485 Pattern.compile(SCRIPT_TAG_END_REGEX,
486 Pattern.CASE_INSENSITIVE);
487
488 text = patternBeginTag.matcher(text)
489 .replaceAll(SCRIPT_TAG_BEGIN_SUBSTITUTE);
490
491 return patternEndTag.matcher(text)
492 .replaceAll(SCRIPT_TAG_END_SUBSTITUTE);
493 }
494}
495
496package net.coderodde.roddenotes.controllers;
497
498import java.io.IOException;
499import java.io.PrintWriter;
500import java.sql.SQLException;
501import javax.servlet.ServletException;
502import javax.servlet.annotation.WebServlet;
503import javax.servlet.http.HttpServlet;
504import javax.servlet.http.HttpServletRequest;
505import javax.servlet.http.HttpServletResponse;
506import net.coderodde.roddenotes.config.Config;
507import net.coderodde.roddenotes.model.Document;
508import net.coderodde.roddenotes.sql.support.MySQLDataAccessObject;
509
510/**
511 * This servlet is responsible for showing the documents via their ID.
512 *
513 * @author Rodion "rodde" Efremov
514 * @version 1.6 (Dec 15, 2017)
515 */
516@WebServlet(name = "ViewServlet", urlPatterns = {"/view"})
517public class ViewServlet extends HttpServlet {
518
519 @Override
520 protected void doGet(HttpServletRequest request,
521 HttpServletResponse response)
522 throws ServletException, IOException {
523 String documentId =
524 request.getParameter(Config.PARAMETERS.DOCUMENT_ID);
525
526 if (documentId == null) {
527 request.getRequestDispatcher(Config.PAGES.NO_ID_VIEW_PAGE)
528 .forward(request, response);
529 return;
530 }
531
532 Document document = null;
533
534 try {
535 document = MySQLDataAccessObject
536 .INSTANCE
537 .getDocument(documentId);
538 } catch (SQLException ex) {
539 throw new RuntimeException(ex);
540 }
541
542 if (document == null) {
543 request.setAttribute(Config.ATTRIBUTES.DOCUMENT_ID,
544 documentId);
545 request.getRequestDispatcher(Config.PAGES.NO_DOCUMENT_PAGE)
546 .forward(request, response);
547 return;
548 }
549
550 request.setAttribute(Config.ATTRIBUTES.DOCUMENT_TEXT,
551 document.getText());
552 request.getRequestDispatcher(Config.PAGES.VIEW_PAGE)
553 .forward(request, response);
554 }
555
556 @Override
557 protected void doPost(HttpServletRequest request,
558 HttpServletResponse response)
559 throws ServletException, IOException {
560 try (PrintWriter out = response.getWriter()) {
561 out.println("This servlet is not accessible via POST method.");
562 }
563 }
564}
565
566package net.coderodde.roddenotes.model;
567
568/**
569 * This class implements a document.
570 *
571 * @author Rodion "rodde" Efremov
572 * @version 1.6 (Dec 15, 2017)
573 */
574public final class Document {
575
576 private String id;
577 private String editToken;
578 private String text;
579
580 public String getId() {
581 return id;
582 }
583
584 public void setId(String id) {
585 this.id = id;
586 }
587
588 public String getEditToken() {
589 return editToken;
590 }
591
592 public void setEditToken(String editToken) {
593 this.editToken = editToken;
594 }
595
596 public String getText() {
597 return text;
598 }
599
600 public void setText(String text) {
601 this.text = text;
602 }
603}
604
605package net.coderodde.roddenotes.sql;
606
607import java.sql.SQLException;
608import net.coderodde.roddenotes.model.Document;
609
610/**
611 * This interface lists all the methods a data access object should implement in
612 * order to integrate with rodde-notes.
613 *
614 * @author Rodion "rodde" Efremov
615 * @version 1.6 (Dec 15, 2017)
616 */
617public interface DataAccessObject {
618
619 /**
620 * Creates a new document with unique ID, random edit token and empty text.
621 *
622 * @return a document.
623 * @throws SQLException if the SQL layer fails.
624 */
625 public Document createNewDocument() throws SQLException;
626
627 /**
628 * Deletes a document with given ID.
629 *
630 * @param id the ID of the document to delete.
631 * @throws SQLException if the SQL layer fails.
632 */
633 public void deleteDocument(String id) throws SQLException;
634
635 /**
636 * Reads a document with given ID.
637 *
638 * @param id the ID of the desired document.
639 * @return the document with the given ID or {@code null}Â if there is no
640 * such.
641 * @throws SQLException if the SQL layer fails.
642 */
643 public Document getDocument(String id) throws SQLException;
644
645 /**
646 * Saves the document. If the document is not yet present in the database,
647 * it is inserted. Otherwise, its state is updated.
648 *
649 * @param document the document to update.
650 * @return {@code true} if the ID and editToken match each other.
651 * {@code false} otherwise.
652 * @throws SQLException if the SQL layer fails.
653 */
654 public boolean updateDocument(Document document) throws SQLException;
655
656 /**
657 * Makes sure all the tables are created in the database.
658 *
659 * @throws SQLException if the SQL layer fails.
660 */
661 public void initializeDatabaseTables() throws SQLException;
662}
663
664package net.coderodde.roddenotes.sql.support;
665
666import java.net.URI;
667import java.net.URISyntaxException;
668import java.sql.Connection;
669import java.sql.DriverManager;
670import java.sql.PreparedStatement;
671import java.sql.ResultSet;
672import java.sql.SQLException;
673import java.sql.Statement;
674import net.coderodde.roddenotes.model.Document;
675import net.coderodde.roddenotes.sql.DataAccessObject;
676import net.coderodde.roddenotes.sql.support.MySQLDefinitions.DELETE;
677import net.coderodde.roddenotes.sql.support.MySQLDefinitions.DOCUMENT_TABLE;
678import net.coderodde.roddenotes.sql.support.MySQLDefinitions.SELECT;
679import net.coderodde.roddenotes.sql.support.MySQLDefinitions.UPDATE;
680import net.coderodde.roddenotes.util.RandomUtilities;
681
682/**
683 * This class implements a data access object over a MySQL database.
684 *
685 * @author Rodion "rodde" Efremov
686 * @version 1.6 (Dec 15, 2017)
687 */
688public final class MySQLDataAccessObject implements DataAccessObject {
689
690 /**
691 * The name of the environment variable holding the connection URI for the
692 * MySQL database server.
693 */
694 private static final String DATABASE_URI_ENVIRONMENT_VARIABLE =
695 "RODDE_NOTES_DB_URI";
696
697 /**
698 * The only instance of this class.
699 */
700 public static final MySQLDataAccessObject INSTANCE =
701 new MySQLDataAccessObject();
702
703 static {
704 try {
705 // Attempts to load the driver.
706 Class.forName("com.mysql.jdbc.Driver");
707 } catch (ClassNotFoundException ex) {
708 throw new RuntimeException("Cannot load the JDBC driver for MySQL.",
709 ex);
710 }
711 }
712
713 private MySQLDataAccessObject() {}
714
715 /**
716 * {@inheritDoc }
717 */
718 @Override
719 public Document createNewDocument() throws SQLException {
720 String id = null;
721 String editToken = RandomUtilities.generateRandomEditToken();
722
723 try (Connection connection = getConnection()) {
724 connection.setAutoCommit(false);
725
726 try (PreparedStatement statement =
727 connection.prepareStatement(MySQLDefinitions.SELECT.DOCUMENT.VIA_DOCUMENT_ID)) {
728
729 while (true) {
730 id = RandomUtilities.generateRandomDocumentId();
731 statement.setString(1, id);
732
733 try (ResultSet resultSet = statement.executeQuery()) {
734 if (!resultSet.next()) {
735 break;
736 }
737 }
738 }
739
740 }
741
742 try (PreparedStatement statement =
743 connection.prepareStatement(
744 MySQLDefinitions.INSERT.DOCUMENT)) {
745
746 statement.setString(1, id);
747 statement.setString(2, editToken);
748 statement.setString(3, ""); // Note the empty text.
749
750 statement.executeUpdate();
751 }
752
753 connection.commit();
754 }
755
756 Document document = new Document();
757 document.setId(id);
758 document.setEditToken(editToken);
759 document.setText(""); // Note the empty text.
760 return document;
761 }
762
763 /**
764 * {@inheritDoc }
765 */
766 @Override
767 public Document getDocument(String id) throws SQLException {
768 try (Connection connection = getConnection()) {
769 try (PreparedStatement statement =
770 connection.prepareStatement(MySQLDefinitions
771 .SELECT
772 .DOCUMENT
773 .VIA_DOCUMENT_ID)) {
774 statement.setString(1, id);
775
776 try (ResultSet resultSet = statement.executeQuery()) {
777 if (!resultSet.next()) {
778 return null;
779 }
780
781 Document document = new Document();
782 document.setId(
783 resultSet.getString(DOCUMENT_TABLE.ID_COLUMN.NAME));
784
785 document.setEditToken(
786 resultSet.getString(
787 DOCUMENT_TABLE.EDIT_TOKEN_COLUMN.NAME));
788
789 document.setText(
790 resultSet.getString(
791 DOCUMENT_TABLE.TEXT_COLUMN.NAME));
792
793 return document;
794 }
795 }
796 }
797 }
798
799 private Connection getConnection() throws SQLException {
800 URI dbUri = null;
801
802 try {
803 dbUri = new URI(System.getenv(DATABASE_URI_ENVIRONMENT_VARIABLE));
804 } catch (URISyntaxException ex) {
805 throw new RuntimeException("Bad URI syntax.", ex);
806 }
807
808 String[] tokens = dbUri.getUserInfo().split(":");
809 String username = tokens[0];
810 String password = tokens[1];
811 String dbUrl = "jdbc:mysql://" + dbUri.getHost() + dbUri.getPath();
812 return DriverManager.getConnection(dbUrl, username, password);
813 }
814
815 /**
816 * {@inheritDoc }
817 */
818 @Override
819 public void initializeDatabaseTables() throws SQLException {
820 try (Connection connection = getConnection()) {
821 try (Statement statement = connection.createStatement()) {
822 statement.executeUpdate(DOCUMENT_TABLE.CREATE_STATEMENT);
823 }
824 }
825 }
826
827 /**
828 * {@inheritDoc }
829 */
830 @Override
831 public boolean updateDocument(Document document) throws SQLException {
832 try (Connection connection = getConnection()) {
833 connection.setAutoCommit(false);
834
835 try (PreparedStatement statement =
836 connection.prepareStatement(
837 SELECT.DOCUMENT.VIA_DOCUMENT_ID)) {
838 statement.setString(1, document.getId());
839
840 try (ResultSet resultSet = statement.executeQuery()) {
841 if (!resultSet.next()) {
842 return false;
843 }
844
845 String editToken =
846 resultSet.getString(
847 DOCUMENT_TABLE.EDIT_TOKEN_COLUMN.NAME);
848
849 if (!editToken.equals(document.getEditToken())) {
850 return false;
851 }
852 }
853 }
854
855 try (PreparedStatement statement =
856 connection.prepareStatement(UPDATE.DOCUMENT.VIA_DOCUMENT_ID)) {
857 statement.setString(1, document.getText());
858 statement.setString(2, document.getId());
859 statement.executeUpdate();
860 }
861
862 connection.commit();
863 }
864
865 return true;
866 }
867
868 @Override
869 public void deleteDocument(String id) throws SQLException {
870 try (Connection connection = getConnection()) {
871 try (PreparedStatement statement =
872 connection.prepareStatement(DELETE.DOCUMENT)) {
873 statement.setString(1, id);
874 statement.executeUpdate();
875 }
876 }
877 }
878}
879
880package net.coderodde.roddenotes.sql.support;
881
882/**
883 * This class defines all the data regarding the database schema for the
884 * rodde-notes app.
885 *
886 * @author Rodion "rodde" Efremov
887 * @version 1.6 (Dec 15, 2017)
888 */
889public final class MySQLDefinitions {
890
891 /**
892 * Defines the structure of the database table holding the note entries.
893 */
894 public static final class DOCUMENT_TABLE {
895
896 /**
897 * The name of the notes table.
898 */
899 public static final String TABLE_NAME = "rodde_notes_documents";
900
901 /**
902 * Describes the note ID column.
903 */
904 public static final class ID_COLUMN {
905
906 /**
907 * The name of the note ID column.
908 */
909 public static final String NAME = "document_id";
910
911 /**
912 * The length of IDs in characters.
913 */
914 public static final int LENGTH = 10;
915
916 /**
917 * The data type of the column.
918 */
919 public static final String TYPE =
920 "CHAR(" + LENGTH + ") NOT NULL";
921 }
922
923 /**
924 * Describes the edit token column.
925 */
926 public static final class EDIT_TOKEN_COLUMN {
927
928 /**
929 * The name of the edit token column.
930 */
931 public static final String NAME = "edit_token";
932
933 /**
934 * The length of edit tokens in characters.
935 */
936 public static final int LENGTH = 12;
937
938 /**
939 * The data type of the column.
940 */
941 public static final String TYPE = "CHAR(" + LENGTH + ") NOT NULL";
942 }
943
944 /**
945 * Describes the text column.
946 */
947 public static final class TEXT_COLUMN {
948
949 /**
950 * The name of the text column.
951 */
952 public static final String NAME = "text";
953
954 /**
955 * The data type of the column.
956 */
957 public static final String TYPE = "TEXT NOT NULL";
958 }
959
960 /**
961 * The SQL statement for creating the note table.
962 */
963 public static final String CREATE_STATEMENT =
964 "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (n" +
965 " " + ID_COLUMN.NAME + " " + ID_COLUMN.TYPE + ",n" +
966 " " + EDIT_TOKEN_COLUMN.NAME + " " +
967 EDIT_TOKEN_COLUMN.TYPE + ",n" +
968 " " + TEXT_COLUMN.NAME + " " + TEXT_COLUMN.TYPE + ",n" +
969 " PRIMARY KEY(" + ID_COLUMN.NAME + "));";
970 }
971
972 /**
973 * Contains all the delete statements.
974 */
975 public static final class DELETE {
976
977 /**
978 * Deletes the document from the database.
979 */
980 public static final String DOCUMENT =
981 "DELETE FROM " + DOCUMENT_TABLE.TABLE_NAME + " WHERE " +
982 DOCUMENT_TABLE.ID_COLUMN.NAME + " = ?;";
983 }
984
985 /**
986 * Contains all the insert statements.
987 */
988 public static final class INSERT {
989
990 /**
991 * Inserts a document.
992 */
993 public static final String DOCUMENT =
994 "INSERT INTO " + DOCUMENT_TABLE.TABLE_NAME +
995 " VALUES (?, ?, ?);";
996 }
997
998 /**
999 * Contains all the select statements.
1000 */
1001 public static final class SELECT {
1002
1003 /**
1004 * Contains all the select statements selecting documents.
1005 */
1006 public static final class DOCUMENT {
1007
1008 /**
1009 * Selects a document via an ID.
1010 */
1011 public static final String VIA_DOCUMENT_ID =
1012 "SELECT * FROM " + DOCUMENT_TABLE.TABLE_NAME + " WHERE " +
1013 DOCUMENT_TABLE.ID_COLUMN.NAME + " = ?;";
1014
1015 /**
1016 * Selects a document via an ID and an edit token.
1017 */
1018 public static final String VIA_DOCUMENT_ID_AND_EDIT_TOKEN =
1019 "SELECT * FROM " + DOCUMENT_TABLE.TABLE_NAME + " WHERE " +
1020 DOCUMENT_TABLE.ID_COLUMN.NAME + " = ? AND " +
1021 DOCUMENT_TABLE.EDIT_TOKEN_COLUMN.NAME + " = ?;";
1022 }
1023 }
1024
1025 /**
1026 * Contains all the update statements.
1027 */
1028 public static final class UPDATE {
1029
1030 /**
1031 * Contains all the update statements on the document.
1032 */
1033 public static final class DOCUMENT {
1034
1035 /**
1036 * Updates the text via the document ID.
1037 */
1038 public static final String VIA_DOCUMENT_ID =
1039 "UPDATE " + DOCUMENT_TABLE.TABLE_NAME + " SET " +
1040 DOCUMENT_TABLE.TEXT_COLUMN.NAME + " = ? WHERE " +
1041 DOCUMENT_TABLE.ID_COLUMN.NAME + " = ?;";
1042 }
1043 }
1044}
1045
1046package net.coderodde.roddenotes.util;
1047
1048import javax.servlet.http.HttpServletRequest;
1049
1050/**
1051 * This class provides various facilities.
1052 *
1053 * @author Rodion "rodde" Efremov
1054 * @version 1.6 (Dec 15, 2017)
1055 */
1056public final class MiscellaneousUtilities {
1057
1058 /**
1059 * Returns the full URL of the web application.
1060 *
1061 * @param request the servlet request object.
1062 * @return an URL.
1063 */
1064 public static String getServerURL(HttpServletRequest request) {
1065 String url = request.getRequestURL().toString();
1066 int lastSlashIndex = url.lastIndexOf('/');
1067 return url.substring(0, lastSlashIndex);
1068 }
1069}
1070
1071package net.coderodde.roddenotes.util;
1072
1073import java.util.concurrent.ThreadLocalRandom;
1074import net.coderodde.roddenotes.sql.support.MySQLDefinitions;
1075
1076/**
1077 * This class provides various utilities for dealing with random strings.
1078 *
1079 * @author Rodion "rodde" Efremov
1080 * @version 1.6 (Dec 15, 2017)
1081 */
1082public final class RandomUtilities {
1083
1084 private static final char[] ALPHABET = new char[62];
1085
1086 static {
1087 int index = 0;
1088
1089 for (char c = '0'; c <= '9'; ++c) {
1090 ALPHABET[index++] = c;
1091 }
1092
1093 for (char c = 'A'; c <= 'Z'; ++c) {
1094 ALPHABET[index++] = c;
1095 }
1096
1097 for (char c = 'a'; c <= 'z'; ++c) {
1098 ALPHABET[index++] = c;
1099 }
1100 }
1101
1102 /**
1103 * Generates a random document ID.
1104 *
1105 * @return a document ID.
1106 */
1107 public static String generateRandomDocumentId() {
1108 return generateRandomString(
1109 MySQLDefinitions.DOCUMENT_TABLE.ID_COLUMN.LENGTH);
1110 }
1111
1112 /**
1113 * Generates a random edit token.
1114 *
1115 * @return an edit token.
1116 */
1117 public static String generateRandomEditToken() {
1118 return generateRandomString(
1119 MySQLDefinitions.DOCUMENT_TABLE.EDIT_TOKEN_COLUMN.LENGTH);
1120 }
1121
1122 /**
1123 * Generates a random string of given length.
1124 *
1125 * @param length the length of the string to generate.
1126 * @return a random string.
1127 */
1128 public static String generateRandomString(int length) {
1129 ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
1130 StringBuilder stringBuilder = new StringBuilder(length);
1131
1132 for (int i = 0; i < length; ++i) {
1133 stringBuilder.append(
1134 ALPHABET[threadLocalRandom.nextInt(ALPHABET.length)]);
1135 }
1136
1137 return stringBuilder.toString();
1138 }
1139}
1140
1141package net.coderodde.roddenotes.util;
1142
1143import java.sql.SQLException;
1144import javax.servlet.ServletContextEvent;
1145import javax.servlet.ServletContextListener;
1146import net.coderodde.roddenotes.sql.support.MySQLDataAccessObject;
1147
1148/**
1149 * This class implements a servlet context listener.
1150 *
1151 * @author Rodion "rodde" Efremov
1152 * @version 1.6 (Dec 15, 2017)
1153 */
1154public final class RoddenoteServletContextListener
1155 implements ServletContextListener {
1156
1157 @Override
1158 public void contextInitialized(ServletContextEvent sce) {
1159 try {
1160 MySQLDataAccessObject.INSTANCE.initializeDatabaseTables();
1161 } catch (SQLException ex) {
1162 throw new RuntimeException(ex);
1163 }
1164 }
1165
1166 @Override
1167 public void contextDestroyed(ServletContextEvent sce) {
1168
1169 }
1170}
1171
1172var RoddeNotes = {};
1173
1174 RoddeNotes.Parameters = {};
1175 RoddeNotes.Parameters.DOCUMENT_ID = "documentId";
1176 RoddeNotes.Parameters.EDIT_TOKEN = "editToken";
1177 RoddeNotes.Parameters.EDITOR_TEXT_AREA = "editorTextArea";
1178
1179 function moveTextToDocument() {
1180 var editorElement =
1181 document.getElementById(
1182 RoddeNotes.Parameters.EDITOR_TEXT_AREA);
1183
1184 var documentViewElement =
1185 document.getElementById("documentView");
1186
1187 var documentText = editorElement.value;
1188 documentViewElement.innerHTML = documentText;
1189 }
1190
1191 function typeset() {
1192 MathJax.Hub.Queue(["Typeset", MathJax.Hub]);
1193 }
1194
1195 function startTypesettingLoop() {
1196 setInterval(function() {typeset();}, 5000);
1197 }
1198
1199 function startSaveLoop() {
1200 setInterval(function() {save();}, 10000);
1201 }
1202
1203 function save() {
1204 var documentId =
1205 document.getElementById(
1206 RoddeNotes.Parameters.DOCUMENT_ID).value;
1207
1208 var editToken =
1209 document.getElementById(
1210 RoddeNotes.Parameters.EDIT_TOKEN).value;
1211
1212 var documentText =
1213 document.getElementById(
1214 RoddeNotes.Parameters.EDITOR_TEXT_AREA).value;
1215 documentText = encodeURIComponent(documentText);
1216 console.log(documentText);
1217
1218 var xhr = new XMLHttpRequest();
1219
1220 xhr.onreadystatechange = function() {
1221 if (this.readyState === 4 && this.status === 200) {
1222 var response = this.responseText;
1223
1224 if (response == "success") {
1225 flashStatusSuccessMessage();
1226 } else if (response == "failure") {
1227 flashStatusFailureMessage();
1228 }
1229 }
1230 };
1231
1232 xhr.open("POST", "update", true);
1233 xhr.setRequestHeader("Content-type",
1234 "application/x-www-form-urlencoded");
1235 xhr.send("documentId=" + documentId +
1236 "&editToken=" + editToken +
1237 "&documentText=" + documentText);
1238 }
1239
1240 function flashStatusSuccessMessage() {
1241 $("#savedSuccessful").fadeIn();
1242 setTimeout(function() {
1243 $("#savedSuccessful").fadeOut();
1244 }, 1500);
1245 }
1246
1247 function flashStatusFailureMessage() {
1248 $("#savedFailed").fadeIn();
1249 setTimeout(function() {
1250 $("#savedFailed").fadeOut();
1251 }, 1500);
1252 }
1253
1254 function deleteDocument() {
1255 var input = prompt("Confirm current document ID:");
1256
1257 var documentId =
1258 document.getElementById(
1259 RoddeNotes.Parameters.DOCUMENT_ID).value;
1260
1261 var editToken =
1262 document.getElementById(
1263 RoddeNotes.Parameters.EDIT_TOKEN).value;
1264
1265 if (documentId != input) {
1266 return;
1267 }
1268
1269 var xhr = new XMLHttpRequest();
1270
1271 xhr.onreadystatechange = function() {
1272 if (this.readyState === 4 && this.status === 200) {
1273 var response = this.responseText;
1274
1275 if (response == "success") {
1276 window.location = "view?documentId=" + documentId;
1277 }
1278 }
1279 };
1280
1281 xhr.open("POST", "deleteDocument", true);
1282 xhr.setRequestHeader("Content-type",
1283 "application/x-www-form-urlencoded");
1284 xhr.send("documentId=" + documentId + "&editToken=" + editToken);
1285 }
1286
1287.topNotifications {
1288 width: 800px;
1289 text-align: center;
1290 vertical-align: central;
1291 border-width: 2px;
1292 border-style: solid;
1293 margin: 0;
1294 padding-top: 15px;
1295 padding-bottom: 15px;
1296 font-family: sans-serif;
1297 font-size: 15px;
1298 margin-bottom: 1px;
1299 display: none;
1300 }
1301
1302 #savedSuccessful {
1303 border-color: darkgreen;
1304 background-color: lightgreen;
1305 color: darkgreen;
1306 }
1307
1308 #savedFailed {
1309 border-color: red;
1310 background-color: pink;
1311 color: red;
1312 }
1313
1314 #documentContainer {
1315 width: 100%;
1316 }
1317
1318 #documentView {
1319 width: 100%;
1320 }
1321
1322 #editorTextArea {
1323 width: 100%;
1324 border-width: 2px;
1325 height: 200px;
1326 margin: 0;
1327 padding: 0;
1328 resize: none;
1329 font-family: monospace;
1330 font-size: 11pt;
1331 }
1332
1333 .button {
1334 width: 100%;
1335 border-width: 2px;
1336 height: 30px;
1337 font-family: sans-serif;
1338 font-size: 15px;
1339 }
1340
1341 #publishLink {
1342 width: 100%;
1343 border: 2px solid blue;
1344 background-color: lightblue;
1345 color: blue;
1346 margin: 0;
1347 margin-top: 3px;
1348 padding-top: 15px;
1349 padding-bottom: 15px;
1350 padding-left: 10px;
1351 padding-right: 10px;
1352 font-family: sans-serif;
1353 font-size: 15px;
1354 display: inline-block;
1355 box-sizing: border-box;
1356 -moz-box-sizing: border-box;
1357 -webkit-box-sizing: border-box;
1358 text-align: left;
1359 overflow: hidden;
1360 }
1361
1362 #publishLinkLabel {
1363 font-family: sans-serif;
1364 font-size: 15px;
1365 padding-right: 15px;
1366 padding-bottom: 7px;
1367 }
1368
1369 #publishLinkContent {
1370 background-color: white;
1371 color: gray;
1372 font-family: monospace;
1373 font-size: 20px;
1374 padding: 3px;
1375 padding-left: 5px;
1376 padding-right: 5px;
1377 }
1378
1379 .error {
1380 color: red;
1381 }
1382
1383<%@page contentType="text/html" pageEncoding="UTF-8"%>
1384<!DOCTYPE html>
1385<html>
1386 <head>
1387 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
1388 <title>rodde-notes</title>
1389 <style>
1390 <%@include file="styles.css" %>
1391 </style>
1392 <script
1393 src="https://code.jquery.com/jquery-3.2.1.min.js"
1394 integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
1395 crossorigin="anonymous">
1396
1397 </script>
1398 <script
1399 src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"
1400 integrity="sha256-VazP97ZCwtekAsvgPBSUwPFKdrwD3unUfSGVYrahUqU="
1401 crossorigin="anonymous">
1402
1403 </script>
1404
1405 <script src='https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.2/MathJax.js?config=TeX-MML-AM_CHTML'></script>
1406
1407 <script type="text/x-mathjax-config">
1408 MathJax.Hub.Config({tex2jax: {inlineMath: [['$','$'], ['\[','\]']]}});
1409 </script>
1410
1411 <script>
1412 <%@include file="code.js" %>
1413 </script>
1414 </head>
1415
1416 <body>
1417 <div id="page">
1418 <div id="documentContainer">
1419 <div id="documentView" align="justify"></div>
1420 <textarea id="editorTextArea" oninput="moveTextToDocument()">${documentText}</textarea>
1421 <button id="typesetButton" class="button" onclick="typeset()">Typeset!</button>
1422 <button id="saveButton" class="button" onclick="save()">Save!</button>
1423 <button id="deleteButton" class="button" onclick="deleteDocument()">Delete!</button>
1424 <div id="publishLink">
1425 <div id="publishLinkLabel">Non-editable publish link:</div>
1426 <div id="publishLinkContent">${publishLink}</div>
1427 </div>
1428 </div>
1429
1430 <form id="dataForm">
1431 <input type="hidden" value="${documentId}" id="documentId" />
1432 <input type="hidden" value="${editToken}" id="editToken" />
1433 </form>
1434
1435 <form id="deleteForm" action="delete" method="post" style="display: none;">
1436 <input type="text" id="idField" name="documentId"/>
1437 <button type="submit">Delete</button>
1438 </form>
1439 <div id ="savedSuccessful" class="topNotifications">
1440 The document is updated.
1441 </div>
1442
1443 <div id="savedFailed" class="topNotifications">
1444 Could not update the document.
1445 </div>
1446 </div>
1447
1448 <script>
1449 startTypesettingLoop();
1450 startSaveLoop();
1451 moveTextToDocument();
1452 </script>
1453 </body>
1454</html>
1455
1456<%@page contentType="text/html" pageEncoding="UTF-8"%>
1457<!DOCTYPE html>
1458<html>
1459 <head>
1460 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
1461 <title>rodde-notes</title>
1462 <style>
1463 #view {
1464 width: 800px;
1465 margin: auto;
1466 }
1467
1468 #text {
1469 text-align: justify;
1470 }
1471 </style>
1472
1473 <script src='https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.2/MathJax.js?config=TeX-MML-AM_CHTML'></script>
1474
1475 <script type="text/x-mathjax-config">
1476 MathJax.Hub.Config({tex2jax: {inlineMath: [['$','$'], ['\[','\]']]}});
1477 </script>
1478 </head>
1479 <body>
1480 <div id="view">
1481 <p id="text">${documentText}</p>
1482 </div>
1483 </body>
1484</html>