0
\$\begingroup\$

I have this blog lurking around in the web. It's just static HTML, but I was interested to keep track of all the views on that blog. Finally, I end up with the following web application:

(The Java backend resides entirely here.)

com.github.coderodde.weblog.viewcounter.CountViewServlet.java

package com.github.coderodde.weblog.viewcounter;

import com.github.coderodde.weblog.viewcounter.exceptions.CannotAddViewException;
import com.github.coderodde.weblog.viewcounter.exceptions.CannotCreateMainTableException;

import static com.github.coderodde.weblog.viewcounter.Utils.objs;
import com.github.coderodde.weblog.viewcounter.exceptions.CannotGetMostRecenetViewTimeException;
import com.github.coderodde.weblog.viewcounter.exceptions.CannotGetNumberOfViewsException;
import com.google.gson.Gson;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.ZonedDateTime;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * This servlet is responsible for storing the IP-address and the timestamp of 
 * a view in at <a href="http://coderodde.github.io/weblog/">coderodde's weblog</a>.
 *
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Aug 21, 2021)
 * @since 1.6 (Aug 21, 2021)
 */
@WebServlet(name="CountViewServlet", urlPatterns={"/countView"})
public final class CountViewServlet extends HttpServlet {

    private static final Gson GSON = new Gson();
    private static final Logger LOGGER =
            Logger.getLogger(CountViewServlet.class.getName());

    @Override
    protected void doPost(HttpServletRequest httpServletRequest,
                          HttpServletResponse httpServletResponse) 
    throws ServletException, IOException {
        // Allow the weblog page to get the response from this servlet:
        httpServletResponse.setHeader("Access-Control-Allow-Origin", 
                                      "coderodde.github.io");

        DataAccessObject dataAccessObject = DataAccessObject.getInstance();
        JSONResponseObject jsonResponseObject = new JSONResponseObject();
        jsonResponseObject.succeeded = false;

        try {
            dataAccessObject.createTablesIfNeeded();
            ZonedDateTime mostRecentViewTime = 
                    dataAccessObject.getMostRecentViewTime();

            if (mostRecentViewTime != null) {
                jsonResponseObject.mostRecentViewTime =
                        mostRecentViewTime.toString();
            }

            dataAccessObject.addView(httpServletRequest); 
            jsonResponseObject.numberOfViews = dataAccessObject.getViewCount();

            // Mark as successful:
            jsonResponseObject.succeeded = true;


        } catch (CannotCreateMainTableException ex) {
            LOGGER.log(
                    Level.SEVERE, 
                    "Could not create the main table: {0}, caused by: {1}", 
                    objs(ex.getCause().getMessage(), 
                         ex.getCause().getCause()));

        } catch (CannotAddViewException ex) {
            LOGGER.log(
                    Level.SEVERE, 
                    "Could not add a view: {0}, caused by: {1}", 
                    objs(ex.getCause().getMessage(), 
                         ex.getCause().getCause()));

        } catch (CannotGetMostRecenetViewTimeException ex) {
            LOGGER.log(
                    Level.SEVERE, 
                    "Could not get the most recent view time: {0}, " + 
                            "caused by: {1}", 
                    objs(ex.getCause().getMessage(), 
                         ex.getCause().getCause()));

        } catch (CannotGetNumberOfViewsException ex) {
            LOGGER.log(
                    Level.SEVERE, 
                    "Could not get the number of views: {0}, caused by: {1}", 
                    objs(ex.getCause().getMessage(), 
                         ex.getCause().getCause()));
        }

        try (PrintWriter printWriter = httpServletResponse.getWriter()) {
            printWriter.print(GSON.toJson(jsonResponseObject));
        }
    }
}

com.github.coderodde.weblog.viewcounter.DataAccessObject.java

package com.github.coderodde.weblog.viewcounter;

import com.github.coderodde.weblog.viewcounter.sql.SQLStatements;
import com.github.coderodde.weblog.viewcounter.exceptions.CannotAddViewException;
import com.github.coderodde.weblog.viewcounter.exceptions.CannotCreateMainTableException;
import com.github.coderodde.weblog.viewcounter.exceptions.CannotGetMostRecenetViewTimeException;
import com.github.coderodde.weblog.viewcounter.exceptions.CannotGetNumberOfViewsException;
import static com.github.coderodde.weblog.viewcounter.Utils.objs;
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 java.sql.Timestamp;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.http.HttpServletRequest;
import org.apache.tomcat.jdbc.pool.DataSource;

/**
 * This class implements the data access object for the view counter.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Aug 22, 2021)
 */
public final class DataAccessObject {

    private static final Logger LOGGER = 
            Logger.getLogger(DataAccessObject.class.getName());

    private static final String EUROPE_HELSINKI_ZONE_ID = "Europe/Helsinki";
    private static final ZoneId ZONE_ID = ZoneId.of(EUROPE_HELSINKI_ZONE_ID);

    private static final String DB_URL_ENVIRONMENT_VARIABLE_NAME = 
            "CLEARDB_DATABASE_URL";

    private static final DataSource dataSource = new DataSource();
    private static final DataAccessObject INSTANCE = new DataAccessObject();

    public static DataAccessObject getInstance() {
        return INSTANCE;
    }

    /**
     * Makes sure that the main table is created.
     * 
     * @throws CannotCreateMainTableException if cannot create the table.
     */
    public void createTablesIfNeeded() throws CannotCreateMainTableException {

        try (Connection connection = getConnection()) {
            connection.createStatement()
                      .executeUpdate(SQLStatements
                                      .ViewTable
                                      .Create
                                      .CREATE_MAIN_TABLE);

        } catch (SQLException cause) {
            LOGGER.log(
                    Level.SEVERE, 
                    "The SQL layer failed: {0}, caused by: {1}", 
                    objs(cause.getMessage(), cause.getCause()));

            throw new CannotCreateMainTableException(cause);
        } catch (URISyntaxException cause) {
            LOGGER.log(
                    Level.SEVERE, 
                    "URI failed: {0}, caused by: {1}", 
                    objs(cause.getMessage(), cause.getCause()));

            throw new CannotCreateMainTableException(cause);
        }
    }

    /**
     * Adds a new view data to the database.
     * 
     * @param httpServletRequest the request object.
     * @throws com.github.coderodde.weblog.viewcounter.exceptions.CannotAddViewException
     * if adding a view data fails.
     */
    public void addView(HttpServletRequest httpServletRequest) 
            throws CannotAddViewException {

        String host = httpServletRequest.getRemoteHost();
        int port = httpServletRequest.getRemotePort();
        String remoteAddress = httpServletRequest.getHeader("X-FORWARDED-FOR");

        if (remoteAddress == null) {
            remoteAddress = httpServletRequest.getRemoteAddr();
        }

        try (Connection connection = getConnection();
             PreparedStatement statement =
                     connection.prepareStatement(
                             SQLStatements.ViewTable.Insert.INSERT_VIEW)) {

            statement.setString(1, remoteAddress);
            statement.setString(2, host);
            statement.setInt(3, port);

            ZonedDateTime nowZonedDateTime = ZonedDateTime.now(ZONE_ID);

            Timestamp nowTimestamp = 
                    Timestamp.from(nowZonedDateTime.toInstant());

            statement.setTimestamp(4, nowTimestamp);
            statement.executeUpdate();
        } catch (Exception cause) {
            throw new CannotAddViewException(cause);
        }
    }

    /**
     * Returns the total number of views. 
     * @return the total number of views so far.
     * @throws com.github.coderodde.weblog.viewcounter.exceptions.CannotGetNumberOfViewsException
     * if cannot get the number of views.
     */
    public int getViewCount() throws CannotGetNumberOfViewsException {
        try (Connection connection = getConnection();
             Statement statement = connection.createStatement()) {

            try (ResultSet resultSet =
                    statement.executeQuery(
                            SQLStatements
                                    .ViewTable
                                    .Select
                                    .GET_NUMBER_OF_VIEWS)) {

                if (!resultSet.next()) {
                    throw new IllegalStateException(
                            "Could not read the number of views.");
                }

                return resultSet.getInt(1);
            }
        } catch (Exception ex) {
            throw new CannotGetNumberOfViewsException(ex);
        }
    }

    /**
     * Returns the most recent view time stamp.
     * @return the most recent view time.
     * @throws CannotGetMostRecenetViewTimeException if reading the most recent
     * view time fails.
     */
    public ZonedDateTime getMostRecentViewTime() 
            throws CannotGetMostRecenetViewTimeException {
        try (Connection connection = getConnection();
             Statement statement = connection.createStatement()) {

            try (ResultSet resultSet = 
                    statement.executeQuery(
                            SQLStatements
                                    .ViewTable
                                    .Select
                                    .GET_MOST_RECENT_VIEW_TIME)) {

                if (!resultSet.next()) {
                    return null;
                }

                Timestamp mostRecentViewTimestamp = resultSet.getTimestamp(1);

                if (mostRecentViewTimestamp == null) {
                    return null;
                }

                ZonedDateTime mostRecentViewZonedDateTime =
                        ZonedDateTime.ofInstant(
                                mostRecentViewTimestamp.toInstant(), 
                                ZONE_ID);

                return mostRecentViewZonedDateTime;
            }

        } catch (SQLException | URISyntaxException ex) {
            throw new CannotGetMostRecenetViewTimeException(ex);
        }
    }

    private static void loadJDBCDriverClass() {
        try {
            Class.forName("com.mysql.cj.jdbc.Driver").newInstance();
        } catch (ClassNotFoundException ex) {
            LOGGER.log(
                    Level.SEVERE, 
                    "com.mysql.cj.jdbc.Driver class not found: {0}, " + 
                            "caused by: {1}", 
                    objs(ex, ex.getCause()));

            throw new RuntimeException(
                    "com.mysql.cj.jdbc.Driver not found.", 
                    ex);

        } catch (InstantiationException ex) {
            LOGGER.log(
                    Level.SEVERE, 
                    "com.mysql.cj.jdbc.Driver could not be instantiated: {0}," +
                            " caused by: {1}", 
                    objs(ex, ex.getCause()));

            throw new RuntimeException(
                    "com.mysql.cj.jdbc.Driver could not be instantiated.", 
                    ex);

        } catch (IllegalAccessException ex) {
            LOGGER.log(
                    Level.SEVERE, 
                    "com.mysql.cj.jdbc.Driver could not be accessed: {0}, " + 
                            "caused by: {1}", 
                    objs(ex, ex.getCause()));

            throw new RuntimeException(
                    "com.mysql.cj.jdbc.Driver could not be accessed.", 
                    ex);
        }
    }

    static {
        loadJDBCDriverClass();
    }

    private Connection getConnection() throws SQLException, URISyntaxException {
        URI databaseURI = 
                new URI(System.getenv(DB_URL_ENVIRONMENT_VARIABLE_NAME));

        String username = databaseURI.getUserInfo().split(":")[0];
        String password = databaseURI.getUserInfo().split(":")[1];
        String databaseURL = 
                "jdbc:mysql://" + databaseURI.getHost() + databaseURI.getPath();

        return DriverManager.getConnection(databaseURL, username, password);
    }
}

com.github.coderodde.weblog.viewcounter.JSONResponseObject.java

package com.github.coderodde.weblog.viewcounter;

/**
 * This POJO class type defines a simple object for reporting to the front-end.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Aug 22, 2021)
 * @since 1.6 (Aug 22, 2021)
 */
public final class JSONResponseObject {

    public boolean succeeded;
    public int numberOfViews;
    public String mostRecentViewTime;
}

com.github.coderodde.weblog.viewcounter.sql.SQLDefinitions.java

package com.github.coderodde.weblog.viewcounter.sql;

/**
 * This class defines the database table schemas.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Aug 22, 2021)
 * @since 1.6 (Aug 22, 2021)
 */
public final class SQLDefinitions {

    /**
     * This class defines the structure of the main table.
     */
    public static final class ViewTable {

        /**
         * The name of the main table.
         */
        public static final String NAME = "view";

        /**
         * This class defines the ID column.
         */
        public static final class Id {
            public static final String NAME = "id";
            public static final String TYPE = "INT NOT NULL AUTO_INCREMENT";
        }

        /**
         * This class defines the IP address column.
         */
        public static final class IPAddress {
            // 16 chars, 15 colons:
            public static final int IPV6_ADDRESS_STRING_LENGTH = 16 + 15;
            public static final String NAME = "ip_address";
            public static final String TYPE = 
                    "VARCHAR(" + IPV6_ADDRESS_STRING_LENGTH + ") NOT NULL";
        }

        /**
         * This class defines the host name column.
         */
        public static final class HostName {
            public static final int MAXIMUM_LENGTH = 253;
            public static final String NAME = "host_name";
            public static final String TYPE =
                    "VARCHAR(" + MAXIMUM_LENGTH + ")"; 
        }

        /**
         * This class defines the port number column.
         */
        public static final class PortNumber {
            public static final String NAME = "port";
            public static final String TYPE = "INT NOT NULL";
        }

        public static final class UserName {
            public static final String NAME = "user_name";
            public static final String TYPE = "VARCHAR(256)";
        }

        /**
         * This class defines the view timestamp.
         */
        public static final class ViewTimestamp {
            public static final String NAME = "viewed_at";
            public static final String TYPE = "TIMESTAMP NOT NULL";
        }
    }
}

com.github.coderodde.weblog.viewcounter.sql.SQLStatements.java

package com.github.coderodde.weblog.viewcounter.sql;

/**
 * This class defines all the SQL statements in the application.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Aug 22, 2021)
 * @since 1.6 (Aug 22, 2021)
 */
public final class SQLStatements {

    /**
     * The statements for the main table.
     */
    public static final class ViewTable {

        /**
         * The create table statements.
         */
        public static final class Create {

            /**
             * Creates a table for storing the views unless there is one already
             * in the database.
             */
            public static final String CREATE_MAIN_TABLE = 
                    "CREATE TABLE IF NOT EXISTS " + 
                    SQLDefinitions.ViewTable.NAME + 
                    " (\n" +
                    SQLDefinitions.ViewTable.Id.NAME + " " +
                    SQLDefinitions.ViewTable.Id.TYPE + ",\n" +
                    SQLDefinitions.ViewTable.IPAddress.NAME + " " + 
                    SQLDefinitions.ViewTable.IPAddress.TYPE + ",\n" +
                    SQLDefinitions.ViewTable.HostName.NAME + " " +
                    SQLDefinitions.ViewTable.HostName.TYPE + ",\n" +
                    SQLDefinitions.ViewTable.PortNumber.NAME + " " +
                    SQLDefinitions.ViewTable.PortNumber.TYPE + ",\n" +
                    SQLDefinitions.ViewTable.UserName.NAME + " " + 
                    SQLDefinitions.ViewTable.UserName.TYPE + ",\n" +
                    SQLDefinitions.ViewTable.ViewTimestamp.NAME + " " + 
                    SQLDefinitions.ViewTable.ViewTimestamp.TYPE + ",\n" +
                    "PRIMARY KEY (" + SQLDefinitions.ViewTable.Id.NAME + ")) " +
                    "ENGINE=InnoDB DEFAULT CHARSET=utf8 DEFAULT COLLATE " + 
                    "utf8_unicode_ci;;";

        }

        /**
         * The insert data statements. 
         */
        public static final class Insert {

            /**
             * Inserts a new view into the database.
             */
            public static final String INSERT_VIEW = 
                    "INSERT INTO `" + SQLDefinitions.ViewTable.NAME + "` (" +
                    SQLDefinitions.ViewTable.IPAddress.NAME + ", " +
                    SQLDefinitions.ViewTable.HostName.NAME + ", " +
                    SQLDefinitions.ViewTable.PortNumber.NAME + ", " +
                    SQLDefinitions.ViewTable.ViewTimestamp.NAME + ") " +
                    "VALUES (?, ?, ?, ?);";
        }

        /**
         * The select data statements.
         */
        public static final class Select {

            /**
             * Returns the total number of views. 
             */
            public static final String GET_NUMBER_OF_VIEWS = 
                    "SELECT COUNT(*) FROM `" + SQLDefinitions.ViewTable.NAME +
                    "`;";

            /**
             * Returns the most recent view time.
             */
            public static final String GET_MOST_RECENT_VIEW_TIME = 
                    "SELECT MAX(" + 
                    SQLDefinitions.ViewTable.ViewTimestamp.NAME + ") FROM " +
                    SQLDefinitions.ViewTable.NAME + ";";
        }
    }
}

At the frontend side, I had to do something like this:

viewCounter.js

function processViewCounter() {
    let span = document.getElementById("view_count_span");
    let xhttp = new XMLHttpRequest();

    xhttp.onreadystatechange = function() {
        if (this.readyState === 4 && this.status === 200) {
            renderResults(this.responseText, span);
        }   
    };

    xhttp.open("POST", "https://weblog-view-counter.herokuapp.com/countView");
    xhttp.send();
}

function getDateString(dateString) {
    const event = new Date(dateString);
    const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', minute: 'numeric', second: 'numeric', hour: 'numeric'};
    return event.toLocaleDateString("en-US", options);
}

function renderResults(jsonText, textElement) {
    const responseObject = JSON.parse(jsonText);

    if (responseObject["succeeded"]) {
        const numberOfViews = responseObject["numberOfViews"];

        if (numberOfViews) {            
            textElement.innerHTML = "Total views: " + numberOfViews + ".";
        } else {
            textElement.innerHTML = "Total views: N/A.";
        }

        let mostRecentViewTime;

        if (responseObject["mostRecentViewTime"]) {
            mostRecentViewTime = responseObject["mostRecentViewTime"].replace("[Europe/Helsinki]", "");
            mostRecentViewTime = getDateString(mostRecentViewTime);
        } else {
            mostRecentViewTime = "N/A";
        }

        textElement.innerHTML += " Last visit time: " + mostRecentViewTime + ".";
    } else {
        textElement.innerHTML = "Total views: N/A. Last visit time: N/A.";
    }
}

Critique request

Please, tell me anything that comes to mind.

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

As you are no beginner, just the terse style I'd also use in my team:

  • Singleton anti-pattern in the DAO. Isn't there a DI container around somewhere?
  • Too verbose with different exception types. The handling is exactly the same apart from different messages, therefore one exception with a message should be enough. (Always be alert if your error handling is longer than the actual code.)
  • final classes. Why? How can you tell today that there never will be a reason to extend the class? (Hint: you can't) Bad idea™.
\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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