org.qi4j.entitystore.sql.SQLEntityStoreMixin.java Source code

Java tutorial

Introduction

Here is the source code for org.qi4j.entitystore.sql.SQLEntityStoreMixin.java

Source

/*
 * Copyright (c) 2010, Stanislav Muhametsin. All Rights Reserved.
 *
 * Licensed 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 org.qi4j.entitystore.sql;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
import org.json.JSONWriter;
import org.qi4j.api.cache.CacheOptions;
import org.qi4j.api.common.Optional;
import org.qi4j.api.common.QualifiedName;
import org.qi4j.api.entity.EntityReference;
import org.qi4j.api.injection.scope.Service;
import org.qi4j.api.injection.scope.Structure;
import org.qi4j.api.injection.scope.This;
import org.qi4j.api.io.Input;
import org.qi4j.api.io.Output;
import org.qi4j.api.io.Receiver;
import org.qi4j.api.io.Sender;
import org.qi4j.api.service.Activatable;
import org.qi4j.api.structure.Application;
import org.qi4j.api.unitofwork.EntityTypeNotFoundException;
import org.qi4j.api.usecase.Usecase;
import org.qi4j.api.usecase.UsecaseBuilder;
import org.qi4j.entitystore.map.MapEntityStore;
import org.qi4j.entitystore.map.MapEntityStoreMixin;
import org.qi4j.entitystore.map.Migration;
import org.qi4j.entitystore.map.StateStore;
import org.qi4j.entitystore.sql.internal.DatabaseSQLService;
import org.qi4j.entitystore.sql.internal.DatabaseSQLService.EntityValueResult;
import org.qi4j.library.sql.api.SQLEntityState;
import org.qi4j.library.sql.api.SQLEntityState.DefaultSQLEntityState;
import org.qi4j.library.sql.common.SQLUtil;
import org.qi4j.spi.entity.EntityDescriptor;
import org.qi4j.spi.entity.EntityState;
import org.qi4j.spi.entity.EntityStatus;
import org.qi4j.spi.entity.EntityType;
import org.qi4j.spi.entity.association.AssociationDescriptor;
import org.qi4j.spi.entitystore.DefaultEntityStoreUnitOfWork;
import org.qi4j.spi.entitystore.EntityNotFoundException;
import org.qi4j.spi.entitystore.EntityStore;
import org.qi4j.spi.entitystore.EntityStoreException;
import org.qi4j.spi.entitystore.EntityStoreSPI;
import org.qi4j.spi.entitystore.EntityStoreUnitOfWork;
import org.qi4j.spi.entitystore.StateCommitter;
import org.qi4j.spi.entitystore.helpers.DefaultEntityState;
import org.qi4j.spi.property.PropertyDescriptor;
import org.qi4j.spi.property.PropertyType;
import org.qi4j.spi.property.PropertyTypeDescriptor;
import org.qi4j.spi.structure.ModuleSPI;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Most of this code is copy-paste from {@link MapEntityStoreMixin}. TODO refactor stuff that has to do with general
 * things than actual MapEntityStore from {@link MapEntityStoreMixin} so that this class could extend some
 * "AbstractJSONEntityStoreMixin".
 *
 */
public class SQLEntityStoreMixin implements EntityStore, EntityStoreSPI, StateStore, Activatable {

    private static final Logger LOGGER = LoggerFactory.getLogger(SQLEntityStoreMixin.class);

    @Service
    private DatabaseSQLService database;

    @This
    private EntityStoreSPI entityStoreSPI;

    @Structure
    private Application application;

    @Optional
    @Service
    private Migration migration;

    private String uuid;

    private Integer count;

    public void activate() throws Exception {
        uuid = UUID.randomUUID().toString() + "-";
        count = 0;
        database.startDatabase();
    }

    public void passivate() throws Exception {
        database.stopDatabase();
    }

    public StateCommitter applyChanges(final EntityStoreUnitOfWork unitofwork, final Iterable<EntityState> states) {
        return new StateCommitter() {

            public void commit() {
                Connection connection = null;
                PreparedStatement insertPS = null;
                PreparedStatement updatePS = null;
                PreparedStatement removePS = null;
                try {
                    connection = database.getConnection();
                    insertPS = database.prepareInsertEntityStatement(connection);
                    updatePS = database.prepareUpdateEntityStatement(connection);
                    removePS = database.prepareRemoveEntityStatement(connection);
                    for (EntityState state : states) {
                        EntityStatus status = state.status();
                        DefaultEntityState defState = ((SQLEntityState) state).getDefaultEntityState();
                        Long entityPK = ((SQLEntityState) state).getEntityPK();
                        if (EntityStatus.REMOVED.equals(status)) {
                            database.populateRemoveEntityStatement(removePS, entityPK, state.identity());
                            removePS.addBatch();
                        } else {
                            StringWriter writer = new StringWriter();
                            writeEntityState(defState, writer, unitofwork.identity());
                            writer.flush();
                            if (EntityStatus.UPDATED.equals(status)) {
                                Long entityOptimisticLock = ((SQLEntityState) state).getEntityOptimisticLock();
                                database.populateUpdateEntityStatement(updatePS, entityPK, entityOptimisticLock,
                                        defState.identity(), writer.toString(), unitofwork.currentTime());
                                updatePS.addBatch();
                            } else if (EntityStatus.NEW.equals(status)) {
                                database.populateInsertEntityStatement(insertPS, entityPK, defState.identity(),
                                        writer.toString(), unitofwork.currentTime());
                                insertPS.addBatch();
                            }
                        }
                    }

                    removePS.executeBatch();
                    insertPS.executeBatch();
                    updatePS.executeBatch();

                    connection.commit();

                } catch (SQLException sqle) {
                    SQLUtil.rollbackQuietly(connection);
                    if (LOGGER.isDebugEnabled()) {
                        StringWriter sb = new StringWriter();
                        sb.append(
                                "SQLException during commit, logging nested exceptions before throwing EntityStoreException:\n");
                        SQLException e = sqle;
                        while (e != null) {
                            e.printStackTrace(new PrintWriter(sb, true));
                            e = e.getNextException();
                        }
                        LOGGER.debug(sb.toString());
                    }
                    throw new EntityStoreException(sqle);
                } catch (RuntimeException re) {
                    SQLUtil.rollbackQuietly(connection);
                    throw new EntityStoreException(re);
                } finally {
                    SQLUtil.closeQuietly(insertPS);
                    SQLUtil.closeQuietly(updatePS);
                    SQLUtil.closeQuietly(removePS);
                    SQLUtil.closeQuietly(connection);
                }
            }

            public void cancel() {
            }

        };
    }

    public EntityState getEntityState(EntityStoreUnitOfWork unitOfWork, EntityReference entityRef) {
        EntityValueResult valueResult = getValue(entityRef);
        return new DefaultSQLEntityState(
                readEntityState((DefaultEntityStoreUnitOfWork) unitOfWork, valueResult.getReader()),
                valueResult.getEntityPK(), valueResult.getEntityOptimisticLock());
    }

    public EntityState newEntityState(EntityStoreUnitOfWork unitOfWork, EntityReference entityRef,
            EntityDescriptor entityDescriptor) {
        return new DefaultSQLEntityState(
                new DefaultEntityState((DefaultEntityStoreUnitOfWork) unitOfWork, entityRef, entityDescriptor),
                database.newPKForEntity(), null);
    }

    public EntityStoreUnitOfWork newUnitOfWork(Usecase usecase, ModuleSPI module, long currentTime) {
        return new DefaultEntityStoreUnitOfWork(entityStoreSPI, newUnitOfWorkId(), module, usecase, currentTime);
    }

    public Input<EntityState, EntityStoreException> entityStates(final ModuleSPI module) {
        return new Input<EntityState, EntityStoreException>() {
            @Override
            public <ReceiverThrowableType extends Throwable> void transferTo(
                    Output<? super EntityState, ReceiverThrowableType> output)
                    throws EntityStoreException, ReceiverThrowableType {
                output.receiveFrom(new Sender<EntityState, EntityStoreException>() {
                    @Override
                    public <ReceiverThrowableType extends Throwable> void sendTo(
                            Receiver<? super EntityState, ReceiverThrowableType> receiver)
                            throws ReceiverThrowableType, EntityStoreException {
                        Connection connection = null;
                        PreparedStatement ps = null;
                        ResultSet rs = null;
                        UsecaseBuilder builder = UsecaseBuilder.buildUsecase("qi4j.entitystore.sql.visit");
                        Usecase usecase = builder.with(CacheOptions.NEVER).newUsecase();
                        final DefaultEntityStoreUnitOfWork uow = new DefaultEntityStoreUnitOfWork(entityStoreSPI,
                                newUnitOfWorkId(), module, usecase, System.currentTimeMillis());
                        try {
                            connection = database.getConnection();
                            ps = database.prepareGetAllEntitiesStatement(connection);
                            database.populateGetAllEntitiesStatement(ps);
                            rs = ps.executeQuery();
                            while (rs.next()) {
                                receiver.receive(readEntityState(uow, database.getEntityValue(rs).getReader()));
                            }
                        } catch (SQLException sqle) {
                            throw new EntityStoreException(sqle);
                        } finally {
                            SQLUtil.closeQuietly(rs);
                            SQLUtil.closeQuietly(ps);
                            SQLUtil.closeQuietly(connection);
                        }

                    }
                });
            }
        };
    }

    @SuppressWarnings("ValueOfIncrementOrDecrementUsed")
    protected String newUnitOfWorkId() {
        return uuid + Integer.toHexString(count++);
    }

    protected DefaultEntityState readEntityState(DefaultEntityStoreUnitOfWork unitOfWork, Reader entityState)
            throws EntityStoreException {
        try {
            ModuleSPI module = unitOfWork.module();
            JSONObject jsonObject = new JSONObject(new JSONTokener(entityState));
            EntityStatus status = EntityStatus.LOADED;

            String version = jsonObject.getString("version");
            long modified = jsonObject.getLong("modified");
            String identity = jsonObject.getString("identity");

            // Check if version is correct
            String currentAppVersion = jsonObject.optString(MapEntityStore.JSONKeys.application_version.name(),
                    "0.0");
            if (!currentAppVersion.equals(application.version())) {
                if (migration != null) {
                    migration.migrate(jsonObject, application.version(), this);
                } else {
                    // Do nothing - set version to be correct
                    jsonObject.put(MapEntityStore.JSONKeys.application_version.name(), application.version());
                }

                LOGGER.trace("Updated version nr on {} from {} to {}",
                        new Object[] { identity, currentAppVersion, application.version() });

                // State changed
                status = EntityStatus.UPDATED;
            }

            String type = jsonObject.getString("type");

            EntityDescriptor entityDescriptor = module.entityDescriptor(type);
            if (entityDescriptor == null) {
                throw new EntityTypeNotFoundException(type);
            }

            Map<QualifiedName, Object> properties = new HashMap<QualifiedName, Object>();
            JSONObject props = jsonObject.getJSONObject("properties");
            for (PropertyDescriptor propertyDescriptor : entityDescriptor.state().properties()) {
                Object jsonValue;
                try {
                    jsonValue = props.get(propertyDescriptor.qualifiedName().name());
                } catch (JSONException e) {
                    // Value not found, default it
                    Object initialValue = propertyDescriptor.initialValue();
                    properties.put(propertyDescriptor.qualifiedName(), initialValue);
                    status = EntityStatus.UPDATED;
                    continue;
                }
                if (jsonValue == JSONObject.NULL) {
                    properties.put(propertyDescriptor.qualifiedName(), null);
                } else {
                    Object value = ((PropertyTypeDescriptor) propertyDescriptor).propertyType().type()
                            .fromJSON(jsonValue, module);
                    properties.put(propertyDescriptor.qualifiedName(), value);
                }
            }

            Map<QualifiedName, EntityReference> associations = new HashMap<QualifiedName, EntityReference>();
            JSONObject assocs = jsonObject.getJSONObject("associations");
            for (AssociationDescriptor associationType : entityDescriptor.state().associations()) {
                try {
                    Object jsonValue = assocs.get(associationType.qualifiedName().name());
                    EntityReference value = jsonValue == JSONObject.NULL ? null
                            : EntityReference.parseEntityReference((String) jsonValue);
                    associations.put(associationType.qualifiedName(), value);
                } catch (JSONException e) {
                    // Association not found, default it to null
                    associations.put(associationType.qualifiedName(), null);
                    status = EntityStatus.UPDATED;
                }
            }

            Map<QualifiedName, List<EntityReference>> manyAssociations = createManyAssociations(jsonObject,
                    entityDescriptor);
            Map<QualifiedName, Map<String, EntityReference>> namedAssociations = createNamedAssociations(jsonObject,
                    entityDescriptor);

            return new DefaultEntityState(unitOfWork, version, modified,
                    EntityReference.parseEntityReference(identity), status, entityDescriptor, properties,
                    associations, manyAssociations, namedAssociations);
        } catch (JSONException e) {
            throw new EntityStoreException(e);
        }
    }

    private Map<QualifiedName, List<EntityReference>> createManyAssociations(JSONObject jsonObject,
            EntityDescriptor entityDescriptor) throws JSONException {
        JSONObject manyAssocs = jsonObject.getJSONObject("manyassociations");
        Map<QualifiedName, List<EntityReference>> manyAssociations = new HashMap<QualifiedName, List<EntityReference>>();
        for (AssociationDescriptor manyAssociationType : entityDescriptor.state().manyAssociations()) {
            List<EntityReference> references = new ArrayList<EntityReference>();
            try {
                JSONArray jsonValues = manyAssocs.getJSONArray(manyAssociationType.qualifiedName().name());
                for (int i = 0; i < jsonValues.length(); i++) {
                    Object jsonValue = jsonValues.getString(i);
                    EntityReference value = jsonValue == JSONObject.NULL ? null
                            : EntityReference.parseEntityReference((String) jsonValue);
                    references.add(value);
                }
                manyAssociations.put(manyAssociationType.qualifiedName(), references);
            } catch (JSONException e) {
                // ManyAssociation not found, default to empty one
                manyAssociations.put(manyAssociationType.qualifiedName(), references);
            }
        }
        return manyAssociations;
    }

    private Map<QualifiedName, Map<String, EntityReference>> createNamedAssociations(JSONObject jsonObject,
            EntityDescriptor entityDescriptor) throws JSONException {
        JSONObject namedAssocs = jsonObject.getJSONObject("namedassociations");
        Map<QualifiedName, Map<String, EntityReference>> namedAssociations = new HashMap<QualifiedName, Map<String, EntityReference>>();
        for (AssociationDescriptor namedAssociationType : entityDescriptor.state().namedAssociations()) {
            Map<String, EntityReference> references = new HashMap<String, EntityReference>();
            try {
                JSONObject jsonValues = namedAssocs.getJSONObject(namedAssociationType.qualifiedName().name());
                for (String name : jsonValues) {
                    Object jsonValue = jsonValues.getString(name);
                    EntityReference value;
                    if (jsonValue == JSONObject.NULL) {
                        value = null;
                    } else {
                        value = EntityReference.parseEntityReference((String) jsonValue);
                    }
                    references.put(name, value);
                }
                namedAssociations.put(namedAssociationType.qualifiedName(), references);
            } catch (JSONException e) {
                // NamedAssociation not found, default to empty one
                namedAssociations.put(namedAssociationType.qualifiedName(), references);
            }
        }
        return namedAssociations;
    }

    public JSONObject getState(String id) throws IOException {
        Reader reader = getValue(EntityReference.parseEntityReference(id)).getReader();
        JSONObject jsonObject;
        try {
            jsonObject = new JSONObject(new JSONTokener(reader));
        } catch (JSONException e) {
            throw new IOException(e);
        }
        reader.close();
        return jsonObject;
    }

    protected EntityValueResult getValue(EntityReference ref) {
        Connection connection = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            connection = database.getConnection();
            ps = database.prepareGetEntityStatement(connection);
            database.populateGetEntityStatement(ps, ref);
            rs = ps.executeQuery();
            if (!rs.next()) {
                throw new EntityNotFoundException(ref);
            }

            EntityValueResult result = database.getEntityValue(rs);

            return result;
        } catch (SQLException sqle) {
            throw new EntityStoreException("Unable to get Entity " + ref, sqle);
        } finally {
            SQLUtil.closeQuietly(rs);
            SQLUtil.closeQuietly(ps);
            SQLUtil.closeQuietly(connection);
        }
    }

    protected void writeEntityState(DefaultEntityState state, Writer writer, String version)
            throws EntityStoreException {
        try {
            JSONWriter json = new JSONWriter(writer);
            JSONWriter properties = json.object().key("identity").value(state.identity().identity())
                    .key("application_version").value(application.version()).key("type")
                    .value(state.entityDescriptor().entityType().type().name()).key("version").value(version)
                    .key("modified").value(state.lastModified()).key("properties").object();
            EntityType entityType = state.entityDescriptor().entityType();
            for (PropertyType propertyType : entityType.properties()) {
                Object value = state.properties().get(propertyType.qualifiedName());
                json.key(propertyType.qualifiedName().name());
                if (value == null) {
                    json.value(null);
                } else {
                    propertyType.type().toJSON(value, json);
                }
            }

            JSONWriter associations = properties.endObject().key("associations").object();
            for (Map.Entry<QualifiedName, EntityReference> stateNameEntityReferenceEntry : state.associations()
                    .entrySet()) {
                EntityReference value = stateNameEntityReferenceEntry.getValue();
                associations.key(stateNameEntityReferenceEntry.getKey().name())
                        .value(value != null ? value.identity() : null);
            }

            JSONWriter manyAssociations = associations.endObject().key("manyassociations").object();
            for (Map.Entry<QualifiedName, List<EntityReference>> stateNameListEntry : state.manyAssociations()
                    .entrySet()) {
                JSONWriter assocs = manyAssociations.key(stateNameListEntry.getKey().name()).array();
                for (EntityReference entityReference : stateNameListEntry.getValue()) {
                    assocs.value(entityReference.identity());
                }
                assocs.endArray();
            }

            JSONWriter namedAssociations = associations.endObject().key("namedassociations").object();
            for (Map.Entry<QualifiedName, Map<String, EntityReference>> stateNameListEntry : state
                    .namedAssociations().entrySet()) {
                JSONWriter assocs = namedAssociations.key(stateNameListEntry.getKey().name()).object();
                Map<String, EntityReference> value = stateNameListEntry.getValue();
                for (Map.Entry<String, EntityReference> entry : value.entrySet()) {
                    assocs.key(entry.getKey()).value(entry.getValue());
                }
                assocs.endObject();
            }
            manyAssociations.endObject().endObject();
        } catch (JSONException e) {
            throw new EntityStoreException("Could not store EntityState", e);
        }
    }

}