hoot.services.models.osm.Changeset.java Source code

Java tutorial

Introduction

Here is the source code for hoot.services.models.osm.Changeset.java

Source

/*
 * This file is part of Hootenanny.
 *
 * Hootenanny is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * --------------------------------------------------------------------
 *
 * The following copyright notices are generated automatically. If you
 * have a new notice to add, please use the format:
 * " * @copyright Copyright ..."
 * This will properly maintain the copyright information. DigitalGlobe
 * copyrights will be updated automatically.
 *
 * @copyright Copyright (C) 2013, 2014, 2015 DigitalGlobe (http://www.digitalglobe.com/)
 */
package hoot.services.models.osm;

import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;

import javax.ws.rs.core.Response.Status;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;

import hoot.services.HootProperties;
import hoot.services.db.DbUtils;
import hoot.services.db.DbUtils.RecordBatchType;
import hoot.services.db2.Changesets;
import hoot.services.db2.QChangesets;
import hoot.services.geo.BoundingBox;
import hoot.services.geo.GeoUtils;
import hoot.services.utils.ResourceErrorHandler;
import hoot.services.utils.XmlDocumentBuilder;

import org.apache.commons.lang3.StringUtils;
import org.apache.xpath.XPathAPI;
import org.joda.time.DateTime;
import org.joda.time.Minutes;
import org.joda.time.Seconds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import com.mysema.query.sql.SQLQuery;
import com.mysema.query.sql.dml.SQLInsertClause;
import com.mysema.query.sql.dml.SQLUpdateClause;

/**
 * Represents the model of an OSM changeset
 *
 * @todo Should this extend Element?
 */
public class Changeset extends Changesets {
    @SuppressWarnings("unused")
    private static final long serialVersionUID = 4011802505587120104L;

    private static final Logger log = LoggerFactory.getLogger(Changeset.class);
    protected static final QChangesets changesets = QChangesets.changesets;
    private int maxRecordBatchSize = -1;
    private Connection conn;
    private long _mapId = -1;

    /**
     * Constructor
     *
     * @param id changeset ID
     * @param conn JDBC Connection
     */
    public Changeset(final long mapId, final long id, final Connection conn) {
        _mapId = mapId;
        setId(id);
        this.conn = conn;
        try {
            maxRecordBatchSize = Integer.parseInt(HootProperties.getInstance().getProperty("maxRecordBatchSize",
                    HootProperties.getDefault("maxRecordBatchSize")));
        } catch (Exception e) {

        }
    }

    /**
     * Creates a new changeset
     *
     * @param changesetDoc changeset create XML
     * @param mapId ID of the map owning the changeset
     * @param userId ID of the user creating the changeset
     * @param conn JDBC Connection
     * @return ID of the created changeset
     * @throws Exception
     */
    public static long createChangeset(final Document changesetDoc, final long mapId, final long userId,
            Connection dbConn) throws Exception {
        final long changesetId = Changeset.insertNew(mapId, userId, dbConn);
        if (changesetId == Long.MAX_VALUE || changesetId < 1) {
            throw new Exception("Invalid changeset ID: " + changesetId);
        }

        (new Changeset(mapId, changesetId, dbConn)).insertTags(mapId,
                XPathAPI.selectNodeList(changesetDoc, "//changeset/tag"), dbConn);

        return changesetId;
    }

    /**
     * Closes a changeset
     *
     * @param changesetId ID of the changeset to close
     * @param conn JDBC Connection
     * @throws Exception
     */
    public static void closeChangeset(final long mapId, final long changesetId, Connection dbConn)
            throws Exception {
        Changeset changeset = new Changeset(mapId, changesetId, dbConn);
        changeset.verifyAvailability();
        final Timestamp now = new Timestamp(Calendar.getInstance().getTimeInMillis());

        new SQLUpdateClause(dbConn, DbUtils.getConfiguration(mapId), changesets)
                .where(changesets.id.eq(changesetId)).set(changesets.closedAt, now).execute();

    }

    /**
     * Determines whether the changeset is open or closed
     *
     * Each changeset is automatically assigned an expiration date when it is created, so this
     * basically checks to see if that expiration has occurred.
     *
     * @return true if the changeset is open; false otherwise
     * @throws IOException
     * @throws NumberFormatException
     */
    public boolean isOpen() throws NumberFormatException, IOException {
        //For some strange reason, Changeset DAO's started not working here after some changes to the
        //review mark logic in ReviewResource.  More specifically, calls to ChangesetDao would return
        //stale data.  I suspect it has something to do with the way the transaction was initialized in
        //ReviewResource, but since I couldn't figure out how to fix it, I changed this code to not
        //use ChangesetDao anymore.

        final Changesets changesetRecord = (Changesets) new SQLQuery(conn, DbUtils.getConfiguration(_mapId))
                .from(changesets).where(changesets.id.eq(getId())).singleResult(changesets);

        final Timestamp now = new Timestamp(Calendar.getInstance().getTimeInMillis());
        return changesetRecord.getClosedAt().after(now)
                && changesetRecord.getNumChanges() < Integer.parseInt(HootProperties.getInstance().getProperty(
                        "maximumChangesetElements", HootProperties.getDefault("maximumChangesetElements")));
    }

    /**
     * Close this changeset
     *
     * @throws Exception
     */
    public void close() throws Exception {
        log.debug("Closing changeset...");
        final Timestamp now = new Timestamp(Calendar.getInstance().getTimeInMillis());

        if (new SQLUpdateClause(conn, DbUtils.getConfiguration(_mapId), changesets).where(changesets.id.eq(getId()))
                .set(changesets.closedAt, now).execute() != 1) {
            throw new Exception("Error closing changeset.");
        }
    }

    /**
     * Updates the expiration of this changeset in the database by modifying its closed at time
     *
     * This logic is pulled directly from the Rails port, and is meant to be executed
     * at the end of each upload process involving this changeset.  This effectively extends the
     * changeset's expiration once any data is written to it and leaves it with a shorter expiration
     * if it has been opened but had no data added to it.
     *
     * @throws Exception
     * @todo This method is very confusing.
     */
    public void updateExpiration() throws Exception {
        final DateTime now = new DateTime();

        //SQLQuery query = new SQLQuery(conn, DbUtils.getConfiguration());

        Changesets changesetRecord = (Changesets) new SQLQuery(conn, DbUtils.getConfiguration(_mapId))
                .from(changesets).where(changesets.id.eq(getId())).singleResult(changesets);

        if (isOpen()) {
            final int maximumChangesetElements = Integer.parseInt(HootProperties.getInstance().getProperty(
                    "maximumChangesetElements", HootProperties.getDefault("maximumChangesetElements")));
            Timestamp newClosedAt = null;
            assert (changesetRecord.getNumChanges() <= maximumChangesetElements);
            if (changesetRecord.getNumChanges() == maximumChangesetElements) {
                newClosedAt = new Timestamp(now.getMillis());
            } else if (changesetRecord.getNumChanges() > 0) {
                /*
                 * from rails port:
                 *
                 * if (closed_at - created_at) > (MAX_TIME_OPEN - IDLE_TIMEOUT)
                 *   self.closed_at = create_at + MAX_TIME_OPEN
                 * else
                 *   self.closed_at = Time.now.getutc + IDLE_TIMEOUT
                 */

                final DateTime createdAt = new DateTime(changesetRecord.getCreatedAt().getTime());
                final DateTime closedAt = new DateTime(changesetRecord.getClosedAt().getTime());

                final int changesetIdleTimeout = Integer.parseInt(HootProperties.getInstance().getProperty(
                        "changesetIdleTimeoutMinutes", HootProperties.getDefault("changesetIdleTimeoutMinutes")));
                final int changesetMaxOpenTime = Integer.parseInt(HootProperties.getInstance().getProperty(
                        "changesetMaxOpenTimeHours", HootProperties.getDefault("changesetMaxOpenTimeHours")));
                //The testChangesetAutoClose option = true causes changesetIdleTimeoutMinutes and
                //changesetMaxOpenTimeHours to be interpreted in seconds rather than minutes and hours,
                //respectively.  This enables faster running of auto-close related unit tests.
                if (Boolean.parseBoolean(HootProperties.getInstance().getProperty("testChangesetAutoClose",
                        HootProperties.getDefault("testChangesetAutoClose")))) {
                    final int changesetMaxOpenTimeSeconds = changesetMaxOpenTime;
                    final int changesetIdleTimeoutSeconds = changesetIdleTimeout;
                    if (Seconds.secondsBetween(createdAt, closedAt)
                            .getSeconds() > (changesetMaxOpenTimeSeconds - changesetIdleTimeoutSeconds)) {
                        newClosedAt = new Timestamp(createdAt.plusSeconds(changesetMaxOpenTimeSeconds).getMillis());
                    } else {
                        newClosedAt = new Timestamp(now.plusSeconds(changesetIdleTimeoutSeconds).getMillis());
                    }
                } else {
                    final int changesetMaxOpenTimeMinutes = changesetMaxOpenTime * 60;
                    final int changesetIdleTimeoutMinutes = changesetIdleTimeout;
                    if (Minutes.minutesBetween(createdAt, closedAt)
                            .getMinutes() > (changesetMaxOpenTimeMinutes - changesetIdleTimeoutMinutes)) {
                        newClosedAt = new Timestamp(createdAt.plusMinutes(changesetMaxOpenTimeMinutes).getMillis());
                    } else {
                        newClosedAt = new Timestamp(now.plusMinutes(changesetIdleTimeoutMinutes).getMillis());
                    }
                }
            }

            if (newClosedAt != null) {
                if (new SQLUpdateClause(conn, DbUtils.getConfiguration(_mapId), changesets)
                        .where(changesets.id.eq(getId())).set(changesets.closedAt, newClosedAt).execute() != 1) {
                    throw new Exception("Error updating expiration on changeset.");
                }
            }
        } else {
            //TODO: I have no idea why this code block is needed now.  It didn't use to be, but after
            //some refactoring to support the changes to marking items as reviewed in ReviewResource, it
            //now is needed.  I've been unable to track down what causes this to happen.
            if (!changesetRecord.getClosedAt().before(new Timestamp(now.getMillis()))) {
                if (new SQLUpdateClause(conn, DbUtils.getConfiguration(_mapId), changesets)
                        .where(changesets.id.eq(getId())).set(changesets.closedAt, new Timestamp(now.getMillis()))
                        .execute() != 1) {
                    throw new Exception("Error updating expiration on changeset.");
                }
            }
        }
    }

    /**
     * Updates the number of changes associated with this changeset in the database
     *
     * @param numChanges the number of changes for the changeset
     * @throws Exception
     */
    public void updateNumChanges(final int numChanges) throws Exception {
        log.debug("Updating num changes...");
        final int maximumChangesetElements = Integer.parseInt(HootProperties.getInstance()
                .getProperty("maximumChangesetElements", HootProperties.getDefault("maximumChangesetElements")));

        Changesets changeset = (Changesets) new SQLQuery(conn, DbUtils.getConfiguration(_mapId)).from(changesets)
                .where(changesets.id.eq(getId())).singleResult(changesets);

        final int currentNumChanges = changeset.getNumChanges();

        assert ((currentNumChanges + numChanges) <= maximumChangesetElements);

        if (new SQLUpdateClause(conn, DbUtils.getConfiguration(_mapId), changesets).where(changesets.id.eq(getId()))
                .set(changesets.numChanges, currentNumChanges + numChanges).execute() != 1) {
            throw new Exception("Error updating num changes.");
        }
    }

    /**
     * Updates a changeset's bounds in the database
     *
     * @param bounds new bounds
     * @throws Exception
     */
    public void setBounds(final BoundingBox bounds) throws Exception {
        log.debug("Updating changeset bounds...");

        if (new SQLUpdateClause(conn, DbUtils.getConfiguration(_mapId), changesets).where(changesets.id.eq(getId()))
                .set(changesets.maxLat, bounds.getMaxLatDb()).set(changesets.maxLon, bounds.getMaxLonDb())
                .set(changesets.minLat, bounds.getMinLatDb()).set(changesets.minLon, bounds.getMinLonDb())
                .execute() != 1) {
            throw new Exception("Error updating changeset bounds.");
        }
    }

    /**
     * Retrieves a changeset's bounds from the database
     *
     * @return changeset bounds
     * @throws Exception
     */
    public BoundingBox getBounds() throws Exception {
        log.debug("Retrieving changeset bounds...");

        Changesets changeset = (Changesets) new SQLQuery(conn, DbUtils.getConfiguration(_mapId)).from(changesets)
                .where(changesets.id.eq(getId())).singleResult(changesets);

        //TODO: I don't like doing this...
        double minLon = DbUtils.fromDbCoordValue(changeset.getMinLon());
        double minLat = DbUtils.fromDbCoordValue(changeset.getMinLat());
        double maxLon = DbUtils.fromDbCoordValue(changeset.getMaxLon());
        double maxLat = DbUtils.fromDbCoordValue(changeset.getMaxLat());
        if (minLon == GeoUtils.DEFAULT_COORD_VALUE || minLat == GeoUtils.DEFAULT_COORD_VALUE
                || maxLon == GeoUtils.DEFAULT_COORD_VALUE || maxLat == GeoUtils.DEFAULT_COORD_VALUE) {
            return new BoundingBox();
        } else {
            //this BoundingBox constructor requires that all values be valid (can't create an invalid
            //empty bounds with this one)
            return new BoundingBox(minLon, minLat, maxLon, maxLat);
        }
    }

    /**
     * Inserts a new empty changeset into the services database
     *
     * @param mapId corresponding map ID for the node
     * @param userId corresponding user ID for the node
     * @param conn JDBC Connection
     * @return ID of the inserted changeset
     * @throws Exception
     */
    public static long insertNew(final long mapId, final long userId, Connection dbConn) throws Exception {
        log.debug("Inserting new changeset...");

        final DateTime now = new DateTime();

        Timestamp closedAt = null;
        final int changesetIdleTimeout = Integer.parseInt(HootProperties.getInstance().getProperty(
                "changesetIdleTimeoutMinutes", HootProperties.getDefault("changesetIdleTimeoutMinutes")));
        //The testChangesetAutoClose option = true causes changesetIdleTimeoutMinutes to be interpreted
        //in seconds rather than minutes and enables faster running of auto-close related unit tests.
        if (Boolean.parseBoolean(HootProperties.getInstance().getProperty("testChangesetAutoClose",
                HootProperties.getDefault("testChangesetAutoClose")))) {
            closedAt = new Timestamp(now.plusSeconds(changesetIdleTimeout).getMillis());
        } else {
            closedAt = new Timestamp(now.plusMinutes(changesetIdleTimeout).getMillis());
        }

        return new SQLInsertClause(dbConn, DbUtils.getConfiguration("" + mapId), changesets)
                .columns(changesets.closedAt, changesets.createdAt, changesets.maxLat, changesets.maxLon,
                        changesets.minLat, changesets.minLon, changesets.userId)
                .values(closedAt, new Timestamp(now.getMillis()),
                        DbUtils.toDbCoordValue(GeoUtils.DEFAULT_COORD_VALUE),
                        DbUtils.toDbCoordValue(GeoUtils.DEFAULT_COORD_VALUE),
                        DbUtils.toDbCoordValue(GeoUtils.DEFAULT_COORD_VALUE),
                        DbUtils.toDbCoordValue(GeoUtils.DEFAULT_COORD_VALUE), userId)
                .executeWithKey(changesets.id);
    }

    /**
     * Determines whether the changeset is available for update
     *
     * @throws Exception
     * @todo verify user updating changeset is the same one that created it; otherwise return 409
     */
    public void verifyAvailability() throws Exception {
        //see comments in isOpen method for why ChangesetDao is not used here anymore
        Changesets changesetRecord = null;
        boolean changesetExists = false;
        try {
            log.debug("Verifying changeset with ID: " + getId() + " has previously been created ...");
            changesetRecord = (Changesets) new SQLQuery(conn, DbUtils.getConfiguration(_mapId)).from(changesets)
                    .where(changesets.id.eq(getId())).singleResult(changesets);
            changesetExists = changesetRecord != null;
        } catch (Exception e) {
            ResourceErrorHandler.handleError(
                    "Error updating changeset with ID: " + getId() + " (" + e.getMessage() + ")",
                    Status.BAD_REQUEST, log);
        }
        if (!changesetExists) {
            //I haven't been able to explicit find in the OSM docs or code what type of response is
            //returned here, but a 404 seems to make sense.
            throw new Exception("Changeset to be updated does not exist with ID: " + getId()
                    + ".  Please create the " + "changeset first.");
        }

        //this handles checking changeset expiration
        if (!isOpen()) {
            //this needs to be retrieved again to refresh the data

            changesetRecord = (Changesets) new SQLQuery(conn, DbUtils.getConfiguration(_mapId)).from(changesets)
                    .where(changesets.id.eq(getId())).singleResult(changesets);
            throw new Exception(
                    "The changeset with ID: " + getId() + " was closed at " + changesetRecord.getClosedAt());
        }
    }

    /**
    * Determines whether the current number of changes associated with this changeset plus some
    * new set of changes exceeds the maximum allowed threshold.
    *
    * @param newChangeCount number of new changes
    * @return true; if the changeset entity count is exceeded; false otherwise
    * @throws IOException if unable to open the services configuration file
    * @throws NumberFormatException
     * @throws TransformerException
    */
    public boolean requestChangesExceedMaxElementThreshold(final Document changesetDiffDoc)
            throws NumberFormatException, IOException, TransformerException {
        final int newChangeCount = XPathAPI.selectNodeList(changesetDiffDoc, "//osmChange/*/node").getLength()
                + XPathAPI.selectNodeList(changesetDiffDoc, "//osmChange/*/way").getLength()
                + XPathAPI.selectNodeList(changesetDiffDoc, "//osmChange/*/relation").getLength();

        //SQLQuery query = new SQLQuery(conn, DbUtils.getConfiguration());
        Changesets changeset = (Changesets) new SQLQuery(conn, DbUtils.getConfiguration(_mapId)).from(changesets)
                .where(changesets.id.eq(getId())).singleResult(changesets);
        return (newChangeCount + changeset.getNumChanges()) > Integer
                .parseInt(HootProperties.getInstance().getProperty("maximumChangesetElements"));
    }

    /**
    * Inserts all tags for an element into the services database
    *
    * @param xml list of XML tags
    * @param conn JDBC Connection
    * @throws Exception
    */
    public void insertTags(final long mapId, final NodeList xml, Connection conn) throws Exception {
        String POSTGRESQL_DRIVER = "org.postgresql.Driver";
        Statement stmt = null;
        try {
            log.debug("Inserting tags for changeset with ID: " + getId());

            String strKv = "";

            for (int i = 0; i < xml.getLength(); i++) {
                NamedNodeMap tagAttributes = xml.item(i).getAttributes();

                String key = "\"" + tagAttributes.getNamedItem("k").getNodeValue() + "\"";
                String val = "\"" + tagAttributes.getNamedItem("v").getNodeValue() + "\"";
                if (strKv.length() > 0) {
                    strKv += ",";
                }

                strKv += key + "=>" + val;

            }
            String strTags = "'";
            strTags += strKv;
            strTags += "'";

            Class.forName(POSTGRESQL_DRIVER);

            stmt = conn.createStatement();

            String sql = "UPDATE changesets_" + mapId + " SET tags = " + strTags + " WHERE id=" + getId();

            stmt.executeUpdate(sql);
        } catch (Exception e) {
            throw new Exception("Error inserting tags for changeset with ID: " + getId() + " - " + e.getMessage());
        }

        finally {
            // finally block used to close resources
            try {
                if (stmt != null)
                    stmt.close();
            } catch (SQLException se2) {

            } // nothing we can do

        } // end try

    }

    /**
     * Creates a simple OSM changeset create XML document
     *
     * @param description an optional changeset description
     * @return a changeset XML document
     * @throws ParserConfigurationException
     * @throws IOException
     * @throws SAXException
     */
    public static Document getChangesetCreateDoc(final String description)
            throws SAXException, IOException, ParserConfigurationException {
        String changesetDocStr = "<osm>" + "<changeset version=\"0.3\" generator=\"hoot-services\">";
        if (StringUtils.trimToNull(description) != null) {
            changesetDocStr += "<tag k=\"description\" v=\"" + description + "\"/>";
        }
        changesetDocStr += "</changeset>" + "</osm>";
        return XmlDocumentBuilder.parse(changesetDocStr);
    }
}