package org.activityinfo.ui.client.local.command; /* * #%L * ActivityInfo Server * %% * Copyright (C) 2009 - 2013 UNICEF * %% * This program 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/gpl-3.0.html>. * #L% */ import com.bedatadriven.rebar.async.AsyncFunction; import com.bedatadriven.rebar.sql.client.*; import com.bedatadriven.rebar.sql.client.fn.AsyncSql; import com.bedatadriven.rebar.sql.client.fn.TxAsyncFunction; import com.bedatadriven.rebar.sql.client.query.SqlInsert; import com.bedatadriven.rebar.sql.client.query.SqlQuery; import com.google.common.base.Function; import com.google.common.collect.Lists; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.inject.Inject; import com.google.inject.Singleton; import org.activityinfo.legacy.shared.command.*; import org.activityinfo.legacy.shared.util.JsonUtil; import org.activityinfo.ui.client.EventBus; import org.activityinfo.ui.client.local.sync.SyncRequestEvent; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import java.util.logging.Logger; import static org.activityinfo.legacy.shared.command.UpdateMonthlyReports.Change; /** * Manages a persistent queue of commands to be sent to the server. */ @Singleton public class CommandQueue { private static Logger LOGGER = Logger.getLogger(CommandQueue.class.getName()); public static class QueueEntry { private int id; private Command command; public QueueEntry(int id, Command command) { super(); this.id = id; this.command = command; } public int getId() { return id; } public void setId(int id) { this.id = id; } public Command getCommand() { return command; } public void setCommand(Command command) { this.command = command; } } private SqlQuery queryNext = SqlQuery.select("id", "command").from("command_queue").orderBy("id"); private SqlQuery queryCount = SqlQuery.select().appendColumn("count(*)", "count").from("command_queue"); private Function<SqlResultSetRowList, Void> fireCount = new Function<SqlResultSetRowList, Void>() { @Override public Void apply(SqlResultSetRowList rows) { eventBus.fireEvent(new CommandQueueEvent(rows.get(0).getInt("count"))); return null; } }; private Function<Void, Void> fireSyncRequest = new Function<Void, Void>() { @Override public Void apply(Void argument) { eventBus.fireEvent(SyncRequestEvent.INSTANCE); return null; } }; private Function<SqlResultSetRow, CommandQueue.QueueEntry> createEntry = new Function<SqlResultSetRow, CommandQueue.QueueEntry>() { @Override public QueueEntry apply(SqlResultSetRow row) { int id = row.getInt("id"); String json = row.getString("command"); return new QueueEntry(id, deserializeCommand(json)); } }; private TxAsyncFunction<CommandQueue.QueueEntry, Void> removeItem = new TxAsyncFunction<CommandQueue.QueueEntry, Void>() { @Override protected void doApply(SqlTransaction tx, QueueEntry argument, final AsyncCallback<Void> callback) { tx.executeSql("DELETE FROM command_queue WHERE id = ?", new Object[]{argument.getId()}, new SqlResultCallback() { @Override public void onSuccess(SqlTransaction tx, SqlResultSet results) { callback.onSuccess(null); } }); } }; private final EventBus eventBus; private final SqlDatabase database; @Inject public CommandQueue(EventBus eventBus, SqlDatabase database) { this.eventBus = eventBus; this.database = database; } public static TxAsyncFunction<Void, Void> createTableIfNotExists() { return AsyncSql.ddl( "CREATE TABLE IF NOT EXISTS command_queue (id INTEGER PRIMARY KEY AUTOINCREMENT, command TEXT)"); } /** * Adds a command to the queue to be executed * * @param cmd * @throws SQLException */ public void queue(SqlTransaction tx, Command cmd) { JsonObject root = serialize(cmd); SqlInsert.insertInto("command_queue") .value("command", root.toString()) .compose(queryCount.asFunction()) .compose(fireCount) .compose(fireSyncRequest) .apply(tx, null); } public AsyncFunction<Void, List<QueueEntry>> get() { return database.asFunction(queryNext.asFunction().mapSequentially(createEntry)); } /** * Peeks at the command next line in for execution, without removing it from * the queue. * * @return the Command next in line for execution */ public void peek(final AsyncCallback<QueueEntry> callback) { get().compose(new Function<List<QueueEntry>, QueueEntry>() { @Override public QueueEntry apply(List<QueueEntry> input) { if (input.isEmpty()) { return null; } else { return input.get(0); } } }).apply(null, callback); } public AsyncFunction<QueueEntry, Void> remove() { return database.asFunction(removeItem.compose(queryCount.asFunction()).compose(fireCount)); } public void remove(QueueEntry entry, AsyncCallback<Void> callback) { remove().apply(entry, callback); } private JsonObject serialize(Command cmd) { if (cmd instanceof CreateSite) { return serialize((CreateSite) cmd); } else if (cmd instanceof UpdateSite) { return serialize((UpdateSite) cmd); } else if (cmd instanceof CreateLocation) { return serialize((CreateLocation) cmd); } else if (cmd instanceof DeleteSite) { return serialize((DeleteSite) cmd); } else if (cmd instanceof UpdateMonthlyReports) { return serialize((UpdateMonthlyReports)cmd); } else { throw new IllegalArgumentException("Cannot serialize commands of type " + cmd.getClass()); } } private JsonObject serialize(DeleteSite cmd) { JsonObject root = new JsonObject(); root.addProperty("commandClass", "DeleteSite"); root.addProperty("id", cmd.getId()); return root; } private JsonObject serialize(CreateSite cmd) { JsonObject root = new JsonObject(); root.addProperty("commandClass", "CreateSite"); root.add("properties", JsonUtil.encodeMap(cmd.getProperties().getTransientMap())); return root; } private JsonObject serialize(UpdateSite cmd) { JsonObject root = new JsonObject(); root.addProperty("commandClass", "UpdateSite"); root.addProperty("siteId", cmd.getSiteId()); root.add("changes", JsonUtil.encodeMap(cmd.getChanges().getTransientMap())); return root; } private JsonObject serialize(CreateLocation cmd) { JsonObject root = new JsonObject(); root.addProperty("commandClass", "CreateLocation"); root.add("properties", JsonUtil.encodeMap(cmd.getProperties().getTransientMap())); return root; } private JsonObject serialize(UpdateMonthlyReports cmd) { JsonArray changeArray = new JsonArray(); for (Change change : cmd.getChanges()) { JsonObject changeObject = new JsonObject(); changeObject.addProperty("year", change.getMonth().getYear()); changeObject.addProperty("month", change.getMonth().getMonth()); changeObject.addProperty("indicatorId", change.getIndicatorId()); changeObject.addProperty("value", change.getValue()); changeArray.add(changeObject); } JsonObject root = new JsonObject(); root.addProperty("commandClass", "UpdateMonthlyReports"); root.addProperty("siteId", cmd.getSiteId()); root.add("changes", changeArray); return root; } private Command deserializeCommand(String json) { JsonObject root = JsonUtil.parse(json); String commandClass = root.get("commandClass").getAsString(); if ("CreateSite".equals(commandClass)) { return deserializeCreateSite(root); } else if ("UpdateSite".equals(commandClass)) { return deserializeUpdateSite(root); } else if ("CreateLocation".equals(commandClass)) { return deserializeCreateLocation(root); } else if ("DeleteSite".equals(commandClass)) { return deserializeDeleteSite(root); } else if ("UpdateMonthlyReports".equals(commandClass)) { return deserializeMonthlyReports(root); } else { throw new RuntimeException("Cannot deserialize queud command of class " + commandClass); } } private Command deserializeMonthlyReports(JsonObject root) { int siteId = root.get("siteId").getAsInt(); JsonArray changeArray = root.get("changes").getAsJsonArray(); ArrayList<Change> changes = Lists.newArrayList(); for(int i=0;i!=changeArray.size();++i) { JsonObject changeObject = changeArray.get(i).getAsJsonObject(); int indicatorId = changeObject.get("indicatorId").getAsInt(); Month month = new Month(changeObject.get("year").getAsInt(), changeObject.get("month").getAsInt()); Double value = null; if(changeObject.get("value").isJsonPrimitive()) { value = changeObject.get("value").getAsDouble(); } changes.add(new Change(indicatorId, month, value)); } return new UpdateMonthlyReports(siteId, changes); } private DeleteSite deserializeDeleteSite(JsonObject root) { DeleteSite cmd = new DeleteSite(); cmd.setId(root.get("id").getAsInt()); return cmd; } private CreateSite deserializeCreateSite(JsonObject root) { return new CreateSite(JsonUtil.decodeMap(root.get("properties").getAsJsonObject())); } private UpdateSite deserializeUpdateSite(JsonObject root) { return new UpdateSite(root.get("siteId").getAsInt(), JsonUtil.decodeMap(root.get("changes").getAsJsonObject())); } private CreateLocation deserializeCreateLocation(JsonObject root) { return new CreateLocation(JsonUtil.decodeMap(root.get("properties").getAsJsonObject())); } }