Advertisement
Guest User

Untitled

a guest
Dec 16th, 2017
123
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 45.14 KB | None | 0 0
  1. package 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. */
  9. public 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.  
  134. package net.coderodde.roddenotes.controllers;
  135.  
  136. import java.io.IOException;
  137. import java.io.PrintWriter;
  138. import java.sql.SQLException;
  139. import javax.servlet.ServletException;
  140. import javax.servlet.annotation.WebServlet;
  141. import javax.servlet.http.HttpServlet;
  142. import javax.servlet.http.HttpServletRequest;
  143. import javax.servlet.http.HttpServletResponse;
  144. import net.coderodde.roddenotes.config.Config;
  145. import net.coderodde.roddenotes.model.Document;
  146. import 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"})
  155. public 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.  
  202. package net.coderodde.roddenotes.controllers;
  203.  
  204. import java.io.IOException;
  205. import java.io.PrintWriter;
  206. import java.sql.SQLException;
  207. import javax.servlet.ServletException;
  208. import javax.servlet.annotation.WebServlet;
  209. import javax.servlet.http.HttpServlet;
  210. import javax.servlet.http.HttpServletRequest;
  211. import javax.servlet.http.HttpServletResponse;
  212. import net.coderodde.roddenotes.config.Config;
  213. import net.coderodde.roddenotes.config.Config.ATTRIBUTES;
  214. import net.coderodde.roddenotes.model.Document;
  215. import net.coderodde.roddenotes.sql.support.MySQLDataAccessObject;
  216. import 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"})
  227. public 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.  
  317. package net.coderodde.roddenotes.controllers;
  318.  
  319. import java.io.IOException;
  320. import java.io.PrintWriter;
  321. import java.sql.SQLException;
  322. import javax.servlet.ServletException;
  323. import javax.servlet.annotation.WebServlet;
  324. import javax.servlet.http.HttpServlet;
  325. import javax.servlet.http.HttpServletRequest;
  326. import javax.servlet.http.HttpServletResponse;
  327. import net.coderodde.roddenotes.config.Config;
  328. import net.coderodde.roddenotes.model.Document;
  329. import 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 = {""})
  339. public 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.  
  391. package net.coderodde.roddenotes.controllers;
  392.  
  393. import java.io.IOException;
  394. import java.io.PrintWriter;
  395. import java.sql.SQLException;
  396. import java.util.regex.Pattern;
  397. import javax.servlet.ServletException;
  398. import javax.servlet.annotation.WebServlet;
  399. import javax.servlet.http.HttpServlet;
  400. import javax.servlet.http.HttpServletRequest;
  401. import javax.servlet.http.HttpServletResponse;
  402. import net.coderodde.roddenotes.config.Config;
  403. import net.coderodde.roddenotes.config.Config.PAGES;
  404. import net.coderodde.roddenotes.config.Config.PARAMETERS;
  405. import net.coderodde.roddenotes.model.Document;
  406. import 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"})
  416. public 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.  
  496. package net.coderodde.roddenotes.controllers;
  497.  
  498. import java.io.IOException;
  499. import java.io.PrintWriter;
  500. import java.sql.SQLException;
  501. import javax.servlet.ServletException;
  502. import javax.servlet.annotation.WebServlet;
  503. import javax.servlet.http.HttpServlet;
  504. import javax.servlet.http.HttpServletRequest;
  505. import javax.servlet.http.HttpServletResponse;
  506. import net.coderodde.roddenotes.config.Config;
  507. import net.coderodde.roddenotes.model.Document;
  508. import 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"})
  517. public 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.  
  566. package 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. */
  574. public 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.  
  605. package net.coderodde.roddenotes.sql;
  606.  
  607. import java.sql.SQLException;
  608. import 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. */
  617. public 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.  
  664. package net.coderodde.roddenotes.sql.support;
  665.  
  666. import java.net.URI;
  667. import java.net.URISyntaxException;
  668. import java.sql.Connection;
  669. import java.sql.DriverManager;
  670. import java.sql.PreparedStatement;
  671. import java.sql.ResultSet;
  672. import java.sql.SQLException;
  673. import java.sql.Statement;
  674. import net.coderodde.roddenotes.model.Document;
  675. import net.coderodde.roddenotes.sql.DataAccessObject;
  676. import net.coderodde.roddenotes.sql.support.MySQLDefinitions.DELETE;
  677. import net.coderodde.roddenotes.sql.support.MySQLDefinitions.DOCUMENT_TABLE;
  678. import net.coderodde.roddenotes.sql.support.MySQLDefinitions.SELECT;
  679. import net.coderodde.roddenotes.sql.support.MySQLDefinitions.UPDATE;
  680. import 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. */
  688. public 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.  
  880. package 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. */
  889. public 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.  
  1046. package net.coderodde.roddenotes.util;
  1047.  
  1048. import 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. */
  1056. public 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.  
  1071. package net.coderodde.roddenotes.util;
  1072.  
  1073. import java.util.concurrent.ThreadLocalRandom;
  1074. import 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. */
  1082. public 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.  
  1141. package net.coderodde.roddenotes.util;
  1142.  
  1143. import java.sql.SQLException;
  1144. import javax.servlet.ServletContextEvent;
  1145. import javax.servlet.ServletContextListener;
  1146. import 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. */
  1154. public 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.  
  1172. var 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>
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement