All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.arakelian.jdbc.store.strategy.HasTimestampJdbcStrategy Maven / Gradle / Ivy

The newest version!
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.arakelian.jdbc.store.strategy;

import java.io.IOException;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.ZonedDateTime;

import org.apache.commons.lang3.StringUtils;

import com.arakelian.core.utils.DateUtils;
import com.arakelian.jdbc.conn.ConnectionFactory;
import com.arakelian.jdbc.utils.DatabaseUtils;
import com.arakelian.json.ImmutableJsonFilterOptions;
import com.arakelian.json.JsonFilter;
import com.arakelian.json.JsonFilterCallback;
import com.arakelian.json.JsonWriter;
import com.arakelian.store.StoreException;
import com.arakelian.store.feature.HasId;
import com.arakelian.store.feature.HasMutableTimestamp;
import com.arakelian.store.feature.HasTimestamp;
import com.arakelian.store.json.StoreObjectMapper;
import com.google.common.base.Preconditions;

public class HasTimestampJdbcStrategy extends AbstractJdbcStrategy {
    /**
     * We do not want to store the created and updated fields inside the JSON document. They're
     * maintained outside in separate fields.
     */
    private static final ImmutableJsonFilterOptions EXCLUDE_CREATED_UPDATED = //
            ImmutableJsonFilterOptions.builder() //
                    .addExcludes("created", "updated") //
                    .build();

    public static String addTimestampsToJson(final String json, final String created, final String updated)
            throws IOException {
        // we add the created and updated fields into the JSON itself, so that our bean, which may
        // use the immutable builder pattern, can have access to them during construction.
        final JsonFilterCallback addCreatedUpdatedFields = new JsonFilterCallback() {
            @Override
            public void beforeEndObject(final JsonFilter filter) throws IOException {
                if (filter.getDepth() == 1) {
                    final JsonWriter writer = filter.getWriter();
                    writer.writeKeyValue("created", created);
                    writer.writeKeyValue("updated", updated);
                }
            }
        };

        // remove existing fields and add correct values
        return JsonFilter.filter(
                StringUtils.defaultIfEmpty(json, "{}"),
                ImmutableJsonFilterOptions.builder() //
                        .addExcludes("created", "updated") //
                        .callback(addCreatedUpdatedFields) //
                        .build());
    }

    public static boolean supports(final Class clazz) {
        return HasTimestamp.class.isAssignableFrom(clazz);
    }

    public HasTimestampJdbcStrategy(final StoreObjectMapper serializer) {
        super(serializer);
    }

    @Override
    public T get(final ResultSet rs) throws SQLException {
        final String json = StringUtils.defaultIfEmpty(rs.getString(1), "{}");
        final String created = rs.getString(2);
        final String updated = rs.getString(3);
        try {
            final String timestamped = addTimestampsToJson(json, created, updated);
            final T bean = mapper.readValue(timestamped);
            return bean;
        } catch (final IOException e) {
            throw new SQLException("Unable to process row", e);
        }
    }

    @Override
    public String getInsertSql(final String table) {
        // we do not use REPLACE INTO syntax because that is technically a DELETE followed by INSERT
        // if the row already exists; this syntax updates a single column
        Preconditions.checkArgument(!StringUtils.isEmpty(table), "table must be non-empty");
        return "insert into " + table
                + "(id,document,created,updated) values(?,?,?,?) on duplicate key update document=?,updated=?";
    }

    @Override
    public String getSelectSql(final String table) {
        Preconditions.checkArgument(!StringUtils.isEmpty(table), "table must be non-empty");
        return "select document, created, updated from " + table;
    }

    @Override
    public final void put(final ConnectionFactory connectionFactory, final String insertSql, final T value)
            throws SQLException {
        // convert value to JSON
        final String json = toString(value);

        // fetch timestamps
        final HasTimestamp hasTimestamp = (HasTimestamp) value;
        final ZonedDateTime c = hasTimestamp.getCreated();
        final ZonedDateTime u = hasTimestamp.getUpdated();

        // assign timestamps if not specified
        final boolean mutable = value instanceof HasMutableTimestamp;
        final ZonedDateTime created;
        final ZonedDateTime updated;
        if (mutable) {
            created = c != null ? c : DateUtils.nowWithZoneUtc();
            updated = u != null ? u : created;
        } else {
            created = Preconditions.checkNotNull(c, "Create date not specified: %s", value);
            updated = Preconditions.checkNotNull(u, "Update date not specified: %s", value);
        }

        // store values
        final String createdString = DateUtils.toStringIsoFormat(created);
        final String updatedString = DateUtils.toStringIsoFormat(updated);

        final int rowsAffected = DatabaseUtils.executeUpdate(
                connectionFactory, //
                insertSql,
                value.getId(),
                json,
                createdString,
                updatedString,
                json,
                updatedString);

        if (mutable) {
            // from MYSQL manual: "With ON DUPLICATE KEY UPDATE, the affected-rows value per row is
            // 1 if the row is inserted as a new row and 2 if an existing row is updated."
            final HasMutableTimestamp hasMutableTimestamp = (HasMutableTimestamp) value;
            switch (rowsAffected) {
            case 1:
                // row inserted
                if (c == null) {
                    hasMutableTimestamp.setCreated(created);
                }
                if (u == null) {
                    hasMutableTimestamp.setUpdated(updated);
                }
                break;
            case 2:
                // row updated
                if (u == null) {
                    hasMutableTimestamp.setUpdated(updated);
                }
                break;
            }
        }
    }

    @Override
    protected String toString(final T value) {
        try {
            // we don't store the created and updated fields inside the JSON itself; those
            // fields are stored outside because our ON DUPLICATE KEY UPDATE logic changes the
            // updated date field, not the json itself.
            final String raw = super.toString(value);
            return JsonFilter.filter(raw, EXCLUDE_CREATED_UPDATED);
        } catch (final IOException e) {
            throw new StoreException(e);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy