diff --git a/src/main/java/com/metaweb/gridlock/GridlockServlet.java b/src/main/java/com/metaweb/gridlock/GridlockServlet.java index 1e8a3e3bf..5ed0a9ee9 100644 --- a/src/main/java/com/metaweb/gridlock/GridlockServlet.java +++ b/src/main/java/com/metaweb/gridlock/GridlockServlet.java @@ -23,6 +23,7 @@ import com.metaweb.gridlock.commands.GetHistoryCommand; import com.metaweb.gridlock.commands.GetProcessesCommand; import com.metaweb.gridlock.commands.GetProjectMetadataCommand; import com.metaweb.gridlock.commands.GetRowsCommand; +import com.metaweb.gridlock.commands.ReconcileCommand; import com.metaweb.gridlock.commands.UndoRedoCommand; public class GridlockServlet extends HttpServlet { @@ -42,6 +43,7 @@ public class GridlockServlet extends HttpServlet { _commands.put("undo-redo", new UndoRedoCommand()); _commands.put("compute-facets", new ComputeFacetsCommand()); _commands.put("do-text-transform", new DoTextTransformCommand()); + _commands.put("reconcile", new ReconcileCommand()); } @Override diff --git a/src/main/java/com/metaweb/gridlock/commands/DoTextTransformCommand.java b/src/main/java/com/metaweb/gridlock/commands/DoTextTransformCommand.java index c07fb26e8..9632e69c3 100644 --- a/src/main/java/com/metaweb/gridlock/commands/DoTextTransformCommand.java +++ b/src/main/java/com/metaweb/gridlock/commands/DoTextTransformCommand.java @@ -10,6 +10,9 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import com.metaweb.gridlock.browsing.Engine; +import com.metaweb.gridlock.browsing.FilteredRows; +import com.metaweb.gridlock.browsing.RowVisitor; import com.metaweb.gridlock.expr.Evaluable; import com.metaweb.gridlock.expr.Parser; import com.metaweb.gridlock.history.CellChange; @@ -27,45 +30,60 @@ public class DoTextTransformCommand extends Command { public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - Project project = getProject(request); - int cellIndex = Integer.parseInt(request.getParameter("cell")); - - Column column = project.columnModel.getColumnByCellIndex(cellIndex); - if (column == null) { - respond(response, "{ \"code\" : \"error\", \"message\" : \"No such column\" }"); - return; - } - - String columnName = column.headerLabel; - String expression = request.getParameter("expression"); - try { + Project project = getProject(request); + Engine engine = getEngine(request, project); + + int cellIndex = Integer.parseInt(request.getParameter("cell")); + Column column = project.columnModel.getColumnByCellIndex(cellIndex); + if (column == null) { + respond(response, "{ \"code\" : \"error\", \"message\" : \"No such column\" }"); + return; + } + + String columnName = column.headerLabel; + String expression = request.getParameter("expression"); + Evaluable eval = new Parser(expression).getExpression(); - //System.out.println("--- " + eval.toString()); Properties bindings = new Properties(); bindings.put("project", project); List cellChanges = new ArrayList(project.rows.size()); - for (int r = 0; r < project.rows.size(); r++) { - Row row = project.rows.get(r); - if (cellIndex < row.cells.size()) { - Cell cell = row.cells.get(cellIndex); - if (cell.value == null) { - continue; - } - - bindings.put("cell", cell); - bindings.put("value", cell.value); - - Cell newCell = new Cell(); - newCell.value = eval.evaluate(bindings); - newCell.recon = cell.recon; - - CellChange cellChange = new CellChange(r, cellIndex, cell, newCell); - cellChanges.add(cellChange); + FilteredRows filteredRows = engine.getAllFilteredRows(); + filteredRows.accept(project, new RowVisitor() { + int cellIndex; + Properties bindings; + List cellChanges; + Evaluable eval; + + public RowVisitor init(int cellIndex, Properties bindings, List cellChanges, Evaluable eval) { + this.cellIndex = cellIndex; + this.bindings = bindings; + this.cellChanges = cellChanges; + this.eval = eval; + return this; } - } + + @Override + public boolean visit(Project project, int rowIndex, Row row) { + if (cellIndex < row.cells.size()) { + Cell cell = row.cells.get(cellIndex); + if (cell.value != null) { + bindings.put("cell", cell); + bindings.put("value", cell.value); + + Cell newCell = new Cell(); + newCell.value = eval.evaluate(bindings); + newCell.recon = cell.recon; + + CellChange cellChange = new CellChange(rowIndex, cellIndex, cell, newCell); + cellChanges.add(cellChange); + } + } + return false; + } + }.init(cellIndex, bindings, cellChanges, eval)); MassCellChange massCellChange = new MassCellChange(cellChanges); HistoryEntry historyEntry = new HistoryEntry( diff --git a/src/main/java/com/metaweb/gridlock/commands/ReconcileCommand.java b/src/main/java/com/metaweb/gridlock/commands/ReconcileCommand.java new file mode 100644 index 000000000..54cb16bd1 --- /dev/null +++ b/src/main/java/com/metaweb/gridlock/commands/ReconcileCommand.java @@ -0,0 +1,82 @@ +package com.metaweb.gridlock.commands; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import com.metaweb.gridlock.browsing.Engine; +import com.metaweb.gridlock.browsing.FilteredRows; +import com.metaweb.gridlock.browsing.RowVisitor; +import com.metaweb.gridlock.model.Cell; +import com.metaweb.gridlock.model.Column; +import com.metaweb.gridlock.model.Project; +import com.metaweb.gridlock.model.Row; +import com.metaweb.gridlock.process.ReconProcess; +import com.metaweb.gridlock.process.ReconProcess.ReconEntry; + +public class ReconcileCommand extends Command { + + @Override + public void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + try { + Project project = getProject(request); + Engine engine = getEngine(request, project); + + int cellIndex = Integer.parseInt(request.getParameter("cell")); + Column column = project.columnModel.getColumnByCellIndex(cellIndex); + if (column == null) { + respond(response, "{ \"code\" : \"error\", \"message\" : \"No such column\" }"); + return; + } + + String columnName = column.headerLabel; + String typeID = request.getParameter("type"); + + List entries = new ArrayList(project.rows.size()); + + FilteredRows filteredRows = engine.getAllFilteredRows(); + filteredRows.accept(project, new RowVisitor() { + int cellIndex; + List entries; + + public RowVisitor init(int cellIndex, List entries) { + this.cellIndex = cellIndex; + this.entries = entries; + return this; + } + + @Override + public boolean visit(Project project, int rowIndex, Row row) { + if (cellIndex < row.cells.size()) { + Cell cell = row.cells.get(cellIndex); + if (cell.value != null) { + entries.add(new ReconEntry(rowIndex, cell)); + } + } + return false; + } + }.init(cellIndex, entries)); + + ReconProcess process = new ReconProcess( + project, + "Reconcile " + columnName + " to type " + typeID, + cellIndex, + entries, + typeID + ); + + boolean done = project.processManager.queueProcess(process); + + respond(response, "{ \"code\" : " + (done ? "\"ok\"" : "\"pending\"") + " }"); + + } catch (Exception e) { + respondException(response, e); + } + } +} diff --git a/src/main/java/com/metaweb/gridlock/history/History.java b/src/main/java/com/metaweb/gridlock/history/History.java index 222e32f23..0663f77c3 100644 --- a/src/main/java/com/metaweb/gridlock/history/History.java +++ b/src/main/java/com/metaweb/gridlock/history/History.java @@ -25,7 +25,11 @@ public class History implements Serializable { } public void addEntry(HistoryEntry entry) { + for (HistoryEntry entry2 : _futureEntries) { + entry2.delete(); + } _futureEntries.clear(); + _pastEntries.add(entry); entry.apply(ProjectManager.singleton.getProject(_projectID)); } @@ -54,6 +58,21 @@ public class History implements Serializable { } } + protected HistoryEntry getEntry(long entryID) { + for (int i = 0; i < _pastEntries.size(); i++) { + if (_pastEntries.get(i).id == entryID) { + return _pastEntries.get(i); + } + } + + for (int i = 0; i < _futureEntries.size(); i++) { + if (_futureEntries.get(i).id == entryID) { + return _futureEntries.get(i); + } + } + return null; + } + protected void undo(int times) { Project project = ProjectManager.singleton.getProject(_projectID); diff --git a/src/main/java/com/metaweb/gridlock/history/HistoryEntry.java b/src/main/java/com/metaweb/gridlock/history/HistoryEntry.java index 4395709f1..1b312546b 100644 --- a/src/main/java/com/metaweb/gridlock/history/HistoryEntry.java +++ b/src/main/java/com/metaweb/gridlock/history/HistoryEntry.java @@ -64,6 +64,13 @@ public class HistoryEntry implements Serializable { _change.revert(project); } + public void delete() { + File file = getFile(); + if (file.exists()) { + file.delete(); + } + } + protected void loadChange() { File file = getFile(); diff --git a/src/main/java/com/metaweb/gridlock/history/HistoryProcess.java b/src/main/java/com/metaweb/gridlock/history/HistoryProcess.java index 2be1d80ef..b8d7e5846 100644 --- a/src/main/java/com/metaweb/gridlock/history/HistoryProcess.java +++ b/src/main/java/com/metaweb/gridlock/history/HistoryProcess.java @@ -1,20 +1,35 @@ package com.metaweb.gridlock.history; +import java.util.Properties; + +import org.json.JSONException; +import org.json.JSONObject; + import com.metaweb.gridlock.model.Project; import com.metaweb.gridlock.process.Process; import com.metaweb.gridlock.process.ProcessManager; public class HistoryProcess extends Process { final protected Project _project; - final protected long _lastDoneID; - + final protected long _lastDoneID; + final protected String _description; + boolean _done = false; + public HistoryProcess(Project project, long lastDoneID) { _project = project; _lastDoneID = lastDoneID; + + if (_lastDoneID == 0) { + _description = "Undo all"; + } else { + HistoryEntry entry = _project.history.getEntry(_lastDoneID); + _description = "Undo/redo until after " + entry.description; + } } @Override public void cancel() { + throw new RuntimeException("Not a long-running process"); } @Override @@ -25,9 +40,32 @@ public class HistoryProcess extends Process { @Override public void performImmediate() { _project.history.undoRedo(_lastDoneID); + _done = true; } @Override public void startPerforming(ProcessManager manager) { + throw new RuntimeException("Not a long-running process"); + } + + @Override + public JSONObject getJSON(Properties options) throws JSONException { + JSONObject o = new JSONObject(); + + o.put("description", _description); + o.put("immediate", true); + o.put("status", _done ? "done" : "pending"); + + return o; + } + + @Override + public boolean isDone() { + throw new RuntimeException("Not a long-running process"); + } + + @Override + public boolean isRunning() { + throw new RuntimeException("Not a long-running process"); } } diff --git a/src/main/java/com/metaweb/gridlock/process/LongRunningProcess.java b/src/main/java/com/metaweb/gridlock/process/LongRunningProcess.java index 9f86a5cbb..c2fccb375 100644 --- a/src/main/java/com/metaweb/gridlock/process/LongRunningProcess.java +++ b/src/main/java/com/metaweb/gridlock/process/LongRunningProcess.java @@ -6,10 +6,11 @@ import org.json.JSONException; import org.json.JSONObject; abstract public class LongRunningProcess extends Process { - final protected String _description; + final protected String _description; protected ProcessManager _manager; protected Thread _thread; protected int _progress; // out of 100 + protected boolean _canceled; protected LongRunningProcess(String description) { _description = description; @@ -17,11 +18,14 @@ abstract public class LongRunningProcess extends Process { @Override public void cancel() { - // TODO Auto-generated method stub - + _canceled = true; + if (_thread != null && _thread.isAlive()) { + _thread.interrupt(); + } } @Override + public JSONObject getJSON(Properties options) throws JSONException { JSONObject o = new JSONObject(); @@ -37,6 +41,16 @@ abstract public class LongRunningProcess extends Process { public boolean isImmediate() { return false; } + + @Override + public boolean isRunning() { + return _thread != null && _thread.isAlive(); + } + + @Override + public boolean isDone() { + return _thread != null && !_thread.isAlive(); + } @Override public void performImmediate() { diff --git a/src/main/java/com/metaweb/gridlock/process/Process.java b/src/main/java/com/metaweb/gridlock/process/Process.java index cc3592955..db118baf6 100644 --- a/src/main/java/com/metaweb/gridlock/process/Process.java +++ b/src/main/java/com/metaweb/gridlock/process/Process.java @@ -16,5 +16,5 @@ public abstract class Process { abstract public void startPerforming(ProcessManager manager); abstract public void cancel(); - abstract JSONObject getJSON(Properties options) throws JSONException; + public abstract JSONObject getJSON(Properties options) throws JSONException; } diff --git a/src/main/java/com/metaweb/gridlock/process/ProcessManager.java b/src/main/java/com/metaweb/gridlock/process/ProcessManager.java index 8922c8c3d..2de629d2f 100644 --- a/src/main/java/com/metaweb/gridlock/process/ProcessManager.java +++ b/src/main/java/com/metaweb/gridlock/process/ProcessManager.java @@ -33,11 +33,15 @@ public class ProcessManager { return true; } else { _processes.add(process); + + update(); + return false; } } public void onDoneProcess(Process p) { + _processes.remove(p); update(); } diff --git a/src/main/java/com/metaweb/gridlock/process/QuickHistoryEntryProcess.java b/src/main/java/com/metaweb/gridlock/process/QuickHistoryEntryProcess.java index f4c73cdf5..f496e8353 100644 --- a/src/main/java/com/metaweb/gridlock/process/QuickHistoryEntryProcess.java +++ b/src/main/java/com/metaweb/gridlock/process/QuickHistoryEntryProcess.java @@ -45,6 +45,7 @@ public class QuickHistoryEntryProcess extends Process { } @Override + public JSONObject getJSON(Properties options) throws JSONException { JSONObject o = new JSONObject(); diff --git a/src/main/java/com/metaweb/gridlock/process/ReconProcess.java b/src/main/java/com/metaweb/gridlock/process/ReconProcess.java new file mode 100644 index 000000000..729205206 --- /dev/null +++ b/src/main/java/com/metaweb/gridlock/process/ReconProcess.java @@ -0,0 +1,73 @@ +package com.metaweb.gridlock.process; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import com.metaweb.gridlock.model.Cell; +import com.metaweb.gridlock.model.Project; + +public class ReconProcess extends LongRunningProcess implements Runnable { + static public class ReconEntry { + final public int rowIndex; + final public Cell cell; + + public ReconEntry(int rowIndex, Cell cell) { + this.rowIndex = rowIndex; + this.cell = cell; + } + } + + final protected Project _project; + final protected int _cellIndex; + final protected List _entries; + final protected String _typeID; + + public ReconProcess(Project project, String description, int cellIndex, List entries, String typeID) { + super(description); + _project = project; + _cellIndex = cellIndex; + _entries = entries; + _typeID = typeID; + } + + @Override + protected Runnable getRunnable() { + return this; + } + + @Override + public void run() { + Map> valueToEntries = new HashMap>(); + + for (ReconEntry entry : _entries) { + Object value = entry.cell.value; + if (value != null && value instanceof String) { + List entries2; + if (valueToEntries.containsKey(value)) { + entries2 = valueToEntries.get(value); + } else { + entries2 = new LinkedList(); + valueToEntries.put((String) value, entries2); + } + entries2.add(entry); + } + } + + List values = new ArrayList(valueToEntries.keySet()); + for (int i = 0; i < values.size(); i += 20) { + _progress = i * 100 / values.size(); + try { + Thread.sleep(100); + } catch (InterruptedException e) { + if (_canceled) { + break; + } + } + } + + _project.processManager.onDoneProcess(this); + } +} diff --git a/src/main/webapp/project.html b/src/main/webapp/project.html index 8cc87d7a1..badd0137a 100644 --- a/src/main/webapp/project.html +++ b/src/main/webapp/project.html @@ -1 +1 @@ - Gridlock
Loading ...
\ No newline at end of file + Gridlock
Loading ...
\ No newline at end of file diff --git a/src/main/webapp/scripts/project/data-table-view.js b/src/main/webapp/scripts/project/data-table-view.js index 7abb32f4d..417dd14e7 100644 --- a/src/main/webapp/scripts/project/data-table-view.js +++ b/src/main/webapp/scripts/project/data-table-view.js @@ -45,6 +45,7 @@ DataTableView.prototype.render = function() { var trHead = table.insertRow(0); var td = trHead.insertCell(trHead.cells.length); + $(td).addClass("column-header"); $('').addClass("column-header-menu").appendTo(td).click(function() { self._createMenuForAllColumns(this); }); @@ -267,7 +268,9 @@ DataTableView.prototype._createMenuForColumnHeader = function(column, index, elm submenu: [ { label: "Initiate on Filtered Rows...", - click: function() {} + click: function() { + new ReconDialog(index); + } }, {}, { @@ -283,7 +286,7 @@ DataTableView.prototype._doTextTransform = function(column, expression) { var self = this; $.post( "/command/do-text-transform?" + $.param({ project: theProject.id, cell: column.cellIndex, expression: expression }), - null, + { engine: JSON.stringify(ui.browsingEngine.getJSON()) }, function(data) { if (data.code == "ok") { self.update(); diff --git a/src/main/webapp/scripts/project/process-widget.js b/src/main/webapp/scripts/project/process-widget.js index e7ee72e21..24d479baa 100644 --- a/src/main/webapp/scripts/project/process-widget.js +++ b/src/main/webapp/scripts/project/process-widget.js @@ -24,9 +24,11 @@ ProcessWidget.prototype._render = function() { this._div.empty(); - var bodyDiv = $('
').addClass("process-panel-inner").text("Testing").appendTo(this._div); + var bodyDiv = $('
').addClass("process-panel-inner").appendTo(this._div); if (this._data.processes.length == 0) { this._div.hide(); + + ui.historyWidget.update(); return; } diff --git a/src/main/webapp/scripts/project/recon-dialog.js b/src/main/webapp/scripts/project/recon-dialog.js new file mode 100644 index 000000000..98ba7baf2 --- /dev/null +++ b/src/main/webapp/scripts/project/recon-dialog.js @@ -0,0 +1,48 @@ +function ReconDialog(columnIndex) { + this._columnIndex = columnIndex; + this._column = theProject.columnModel.columns[columnIndex]; + + this._createDialog(); +} + +ReconDialog.prototype._createDialog = function() { + var self = this; + var frame = DialogSystem.createDialog(); + frame.width("400px"); + + var header = $('
').addClass("dialog-header").text("Reconcile column " + this._column.headerLabel).appendTo(frame); + var body = $('
').addClass("dialog-body").appendTo(frame); + var footer = $('
').addClass("dialog-footer").appendTo(frame); + + $('

').text("Reconcile cell values to topics of type:").appendTo(body); + + var type = null; + var input = $('').appendTo($('

').appendTo(body)); + input.suggest({ type : '/type/type' }).bind("fb-select", function(e, data) { + type = data.id; + }); + + $('').text("Start Reconciling").click(function() { + DialogSystem.dismissUntil(level - 1); + $.post( + "/command/reconcile?" + $.param({ project: theProject.id, cell: self._column.cellIndex, type: type }), + { engine: JSON.stringify(ui.browsingEngine.getJSON()) }, + function(data) { + if (data.code != "error") { + ui.processWidget.update(); + } else { + alert(data.message); + } + }, + "json" + ); + }).appendTo(footer); + + $('').text("Cancel").click(function() { + DialogSystem.dismissUntil(level - 1); + }).appendTo(footer); + + var level = DialogSystem.showDialog(frame); + + input[0].focus(); +}; diff --git a/src/main/webapp/scripts/util/dialog.js b/src/main/webapp/scripts/util/dialog.js new file mode 100644 index 000000000..df75cb98a --- /dev/null +++ b/src/main/webapp/scripts/util/dialog.js @@ -0,0 +1,70 @@ +DialogSystem = { + _layers: [], + _bottomOverlay: null +}; + +DialogSystem.showDialog = function(elmt, onCancel) { + if (DialogSystem._bottomOverlay == null) { + DialogSystem._bottomOverlay = $('
 
') + .addClass("dialog-overlay") + .css("z-index", 100) + .appendTo(document.body); + } + + var overlay = $('
 
') + .addClass("dialog-overlay2") + .css("z-index", 101 + DialogSystem._layers.length * 2) + .appendTo(document.body); + + var container = $('
') + .addClass("dialog-container") + .css("z-index", 102 + DialogSystem._layers.length * 2) + .appendTo(document.body); + + elmt.css("visibility", "hidden").appendTo(container); + container.css("top", Math.round((overlay.height() - elmt.height()) / 2) + "px"); + elmt.css("visibility", "visible"); + + var layer = { + overlay: overlay, + container: container, + onCancel: onCancel + }; + DialogSystem._layers.push(layer); + + var level = DialogSystem._layers.length; + return level; +}; + +DialogSystem.dismissAll = function() { + DialogSystem.dismissUntil(0); +}; + +DialogSystem.dismissUntil = function(level) { + for (var i = DialogSystem._layers.length - 1; i >= level; i--) { + var layer = DialogSystem._layers[i]; + layer.overlay.remove(); + layer.container.remove(); + + if (layer.onCancel) { + try { + layer.onCancel(); + } catch (e) { + console.log(e); + } + } + } + DialogSystem._layers = DialogSystem._layers.slice(0, level); + + if (level == 0) { + if (DialogSystem._bottomOverlay != null) { + DialogSystem._bottomOverlay.remove(); + DialogSystem._bottomOverlay = null; + } + } +}; + +DialogSystem.createDialog = function() { + return $('
').addClass("dialog-frame"); +}; + diff --git a/src/main/webapp/styles/common.css b/src/main/webapp/styles/common.css index 0bebb5fed..6b164bfa4 100644 --- a/src/main/webapp/styles/common.css +++ b/src/main/webapp/styles/common.css @@ -89,3 +89,64 @@ a.menu-item:hover { a.menu-item img { border: none; } + +.dialog-overlay { + background: black; + opacity: 0.3; + position: fixed; + padding: 0px; + margin: 0px; + top: 0px; + left: 0px; + width: 100%; + height: 100%; +} + +.dialog-overlay2 { + position: fixed; + padding: 0px; + margin: 0px; + top: 0px; + left: 0px; + width: 100%; + height: 100%; + background: black; + opacity: 0.01; +} + +.dialog-container { + position: fixed; + padding: 0px; + margin: 0px; + left: 0px; + width: 100%; + text-align: center; +} + +.dialog-frame { + margin: 0 auto; + text-align: left; + background: white; + border: 1px solid #aaa; + padding: 2px; +} + +.dialog-header { + background: #ccc; + padding: 5px 10px; + font-weight: bold; +} + +.dialog-body { + padding: 10px; +} + +.dialog-footer { + background: #ddd; + padding: 5px 10px; + text-align: right; +} + +.dialog-footer button { + margin-left: 5px; +} \ No newline at end of file diff --git a/src/main/webapp/styles/process.css b/src/main/webapp/styles/process.css index 90923fef7..7c89b2f51 100644 --- a/src/main/webapp/styles/process.css +++ b/src/main/webapp/styles/process.css @@ -12,4 +12,8 @@ background: #fffee0; margin: 0 auto; text-align: left; +} + +.fbs-pane, .fbs-flyout-pane { + z-index: 200; } \ No newline at end of file