@Override
  public void revert(Project project) {
    synchronized (project) {
      project.rows.clear();
      project.rows.addAll(_oldRows);

      for (int i = 0; i < _columnNames.size(); i++) {
        project.columnModel.columns.remove(_columnInsertIndex);
      }

      project.update();
    }
  }
  @Override
  public void doGet(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {

    try {
      Project project = getProject(request);

      List<String> localExpressions =
          toExpressionList(project.getMetadata().getPreferenceStore().get("scripting.expressions"));
      localExpressions =
          localExpressions.size() > 20 ? localExpressions.subList(0, 20) : localExpressions;

      List<String> globalExpressions =
          toExpressionList(
              ProjectManager.getSingleton().getPreferenceStore().get("scripting.expressions"));
      Set<String> starredExpressions =
          new HashSet<String>(
              ((TopList)
                      ProjectManager.getSingleton()
                          .getPreferenceStore()
                          .get("scripting.starred-expressions"))
                  .getList());

      Set<String> done = new HashSet<String>();

      response.setCharacterEncoding("UTF-8");
      response.setHeader("Content-Type", "application/json");

      JSONWriter writer = new JSONWriter(response.getWriter());
      writer.object();
      writer.key("expressions");
      writer.array();
      for (String s : localExpressions) {
        writer.object();
        writer.key("code");
        writer.value(s);
        writer.key("global");
        writer.value(false);
        writer.key("starred");
        writer.value(starredExpressions.contains(s));
        writer.endObject();
        done.add(s);
      }
      for (String s : globalExpressions) {
        if (!done.contains(s)) {
          writer.object();
          writer.key("code");
          writer.value(s);
          writer.key("global");
          writer.value(true);
          writer.key("starred");
          writer.value(starredExpressions.contains(s));
          writer.endObject();
        }
      }
      writer.endArray();
      writer.endObject();
    } catch (Exception e) {
      respondException(response, e);
    }
  }
  @Override
  public void apply(Project project) {
    synchronized (project) {
      if (_firstNewCellIndex < 0) {
        _firstNewCellIndex = project.columnModel.allocateNewCellIndex();
        for (int i = 1; i < _columnNames.size(); i++) {
          project.columnModel.allocateNewCellIndex();
        }

        _oldRows = new ArrayList<Row>(project.rows);

        _newRows = new ArrayList<Row>(project.rows.size());

        int cellIndex = project.columnModel.getColumnByName(_baseColumnName).getCellIndex();
        int keyCellIndex =
            project.columnModel.columns.get(project.columnModel.getKeyColumnIndex()).getCellIndex();
        int index = 0;

        int rowIndex = index < _rowIndices.size() ? _rowIndices.get(index) : _oldRows.size();
        DataExtension dataExtension =
            index < _rowIndices.size() ? _dataExtensions.get(index) : null;

        index++;

        Map<String, Recon> reconMap = new HashMap<String, Recon>();

        for (int r = 0; r < _oldRows.size(); r++) {
          Row oldRow = _oldRows.get(r);
          if (r < rowIndex) {
            _newRows.add(oldRow.dup());
            continue;
          }

          if (dataExtension == null || dataExtension.data.length == 0) {
            _newRows.add(oldRow);
          } else {
            Row firstNewRow = oldRow.dup();
            extendRow(firstNewRow, dataExtension, 0, reconMap);
            _newRows.add(firstNewRow);

            int r2 = r + 1;
            for (int subR = 1; subR < dataExtension.data.length; subR++) {
              if (r2 < project.rows.size()) {
                Row oldRow2 = project.rows.get(r2);
                if (oldRow2.isCellBlank(cellIndex) && oldRow2.isCellBlank(keyCellIndex)) {

                  Row newRow = oldRow2.dup();
                  extendRow(newRow, dataExtension, subR, reconMap);

                  _newRows.add(newRow);
                  r2++;

                  continue;
                }
              }

              Row newRow = new Row(cellIndex + _columnNames.size());
              extendRow(newRow, dataExtension, subR, reconMap);

              _newRows.add(newRow);
            }

            r = r2 - 1; // r will be incremented by the for loop anyway
          }

          rowIndex = index < _rowIndices.size() ? _rowIndices.get(index) : _oldRows.size();
          dataExtension = index < _rowIndices.size() ? _dataExtensions.get(index) : null;
          index++;
        }
      }

      project.rows.clear();
      project.rows.addAll(_newRows);

      for (int i = 0; i < _columnNames.size(); i++) {
        String name = _columnNames.get(i);
        int cellIndex = _firstNewCellIndex + i;

        Column column = new Column(cellIndex, name);
        column.setReconConfig(new DBpediaDataExtensionReconConfig(_columnTypes.get(i)));
        column.setReconStats(ReconStats.create(project, cellIndex));

        try {
          project.columnModel.addColumn(_columnInsertIndex + i, column, true);

          // the column might have been renamed to avoid collision
          _columnNames.set(i, column.getName());
        } catch (ModelException e) {
          // won't get here since we set the avoid collision flag
        }
      }

      project.update();
    }
  }