src/org/myorg/myscclient/CheckStructureWSAction.java
package org.myorg.myscclient;

import chemaxon.formats.MolExporter;
import chemaxon.formats.MolImporter;
import chemaxon.struc.MPropertyContainer;
import chemaxon.struc.Molecule;
import com.google.common.base.Objects;
import com.im.commons.db.ddl.DBDatabaseInfo;
import com.im.commons.progress.DFEnvironmentRO;
import com.im.commons.progress.DFEnvironmentRW;
import com.im.commons.progress.DFFeedback.Type;
import com.im.commons.progress.DFLock;
import com.im.commons.progress.EnvUtils;
import com.im.df.api.capabilities.DFFieldStructureCapability;
import com.im.df.api.capabilities.JChemEntityCapability;
import com.im.df.api.chem.MarvinStructure;
import com.im.df.api.ddl.DFEntity;
import com.im.df.api.ddl.DFField;
import com.im.df.api.ddl.DFItems;
import com.im.df.api.dml.DFEntityDataProvider;
import com.im.df.api.dml.DFResultSet;
import com.im.df.api.support.DFUndoConfig;
import com.im.df.api.support.DFUpdateDescription;
import com.im.df.api.util.DIFUtilities;
import com.im.df.impl.db.api.DBImplConstants;
import com.im.df.util.UIBackgroundRunnerRO;
import com.im.ijc.core.api.actions.AbstractFieldSelectionAwareAction;
import com.im.ijc.core.api.views.IJCWidget;
import com.im.ijc.core.api.util.IJCCoreUtils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.openide.DialogDescriptor;
import org.openide.DialogDisplayer;
import org.openide.nodes.Node;
import org.openide.util.NbBundle;

/**
 * Structure check action which is invoking the web service.
 */
public class CheckStructureWSAction extends AbstractFieldSelectionAwareAction {

    /** Validated molecules returned from web service will be stored in column (field) with this name */
    private static final String STRUCTURE_AFTER_WS_CHECK = "STRUCTURE_2"; // NOI18N

    /** Prefix for newly created columns type: integer */
    private static final String TYPE_INT = "[Int]"; // NOI18N

    /** Prefix for newly created columns type: text */
    private static final String TYPE_TEXT = "[Text]"; // NOI18N

    public CheckStructureWSAction() {
    }

    @Override
    protected void performAction(Node[] nodes) {
        IJCWidget widget = findWidget(nodes);

        final DFResultSet.VertexState vs = IJCCoreUtils.computeCommonVS(widget);
        final DFEntity entity = vs.getVertex().getEntity();

        UIBackgroundRunnerRO runner = new UIBackgroundRunnerRO(NbBundle.getMessage(CheckStructureWSAction.class, "MSG_RunningValidationWS"), true) {
            @Override
            public void phase1InRequestProcessor() {
                String msg = null;
                try {
                    SCWSClient client = new SCWSClient();
                    // Let's say 500 units is complete progress. Each phase has 100 steps.
                    getEnvironment().getFeedback().switchToDeterminate(500);

                    ClientUtilities.reportProgress(getEnvironment(), "MSG_PreparingData", 0);
                    List<? extends Comparable<?>> selectedRowsIds = vs.getSelectedRowsIds();
                    Map<Comparable<?>, Map<String, Object>> data = vs.getData(selectedRowsIds, getEnvironment());

                    ClientUtilities.reportProgress(getEnvironment(), "MSG_GeneratingSDF", 100);
                    String sdf = prepareSDF(entity, data, getEnvironment());

                    ClientUtilities.reportProgress(getEnvironment(), "MSG_CallingWebService", 200);
                    String result = client.validate(sdf);

                    ClientUtilities.reportProgress(getEnvironment(), "MSG_PreparingDataForSave", 300);
                    List<DFUpdateDescription> updates = parseResult(entity, data, result, getEnvironment());

                    if (!updates.isEmpty()) {
                        ClientUtilities.reportProgress(getEnvironment(), "MSG_SavingData", 400);
                        doUpdate(entity, updates, getEnvironment());
                    }

                    ClientUtilities.reportProgress(getEnvironment(), "MSG_Done", 500);
                } catch (HTTPLevelErrorException exc) {
                    msg = NbBundle.getMessage(CheckStructureWSAction.class, "ERR_Network",
                            exc.getStatusCode(), exc.getStatusDescription());
                } catch (ServiceLevelException exc) {
                    msg = NbBundle.getMessage(CheckStructureWSAction.class, "ERR_WebServiceInternal",
                            exc.getType().getCode(), exc.getType().getMessage(), exc.getDetails());
                } catch (Exception exc) {
                    msg = NbBundle.getMessage(CheckStructureWSAction.class, "ERR_GenericIO", exc.getMessage());
                }
                if (msg != null) {
                    DialogDescriptor.Message dd = new DialogDescriptor.Message(msg, DialogDescriptor.ERROR_MESSAGE);
                    DialogDisplayer.getDefault().notifyLater(dd);
                    getEnvironment().getFeedback().addMessage(Type.INFO, msg, null);
                    ClientUtilities.reportProgress(getEnvironment(), "MSG_Failed", 400);
                }
            }

        };
        runner.start();
    }

    @Override
    protected boolean enableAccordingToCurrentSelection(IJCWidget widget) {
        if (super.enableAccordingToCurrentSelection(widget)) {
            DFResultSet.VertexState vs = IJCCoreUtils.computeCommonVS(widget);
            if (vs == null) {
                // only when all selected widgets are bound to the same entity
                return false;
            }

            if (DIFUtilities.findCapability(vs.getVertex().getEntity(), JChemEntityCapability.class) == null) {
                // Works only for structure (jchem) tables
                return false;
            }

            // Maybe also test if right columns exist in this entity
            return true;
        }
        return false;
    }

    @Override
    public String getName() {
        // Name of the action
        return NbBundle.getMessage(CheckStructureWSAction.class, "NAME_StructureCheckWS");
    }

    /**
     * Generate SDF from current result set.
     *
     * @param entity Database table (entity)
     * @param data Data Map< rowId, Map< fieldId, value >>
     * @return SDF output as String
     * @throws IOException if any problem occurs during sdf generation
     */
    private static String prepareSDF(DFEntity entity, Map<Comparable<?>, Map<String, Object>> data, DFEnvironmentRO env) throws IOException {
        List<DFField> fields = entity.getFields().getItems();

        // Map < idOfField, nameOfField >
        List<DFField> fieldsForExport = ClientUtilities.prepareFieldsForExport(entity);

        // Only one structure field is supported, so it's possible to use get(0)
        DFField structureField = DFItems.findItemsWithCapability(fields, DFFieldStructureCapability.class).get(0);
        String structureFieldId = structureField.getId();

        ClientUtilities.StringBufferOutputStream out = new ClientUtilities.StringBufferOutputStream();
        MolExporter exporter = new MolExporter(out, "sdf"); // NOI18N

        int totalRow = data.size();
        int count = 0;
        Iterator<Entry<Comparable<?>, Map<String, Object>>> iterator = data.entrySet().iterator();
        while (iterator.hasNext()) {
            Entry<Comparable<?>, Map<String, Object>> row = iterator.next();
            Object structure = row.getValue().get(structureFieldId);
            if (structure instanceof MarvinStructure) {
                MarvinStructure mStruct = (MarvinStructure) structure;
                Molecule mol = mStruct.getNative();
                MPropertyContainer properties = mol.properties();
                for (DFField f: fieldsForExport) {
                    Object value = row.getValue().get(f.getId());
                    properties.setObject(ClientUtilities.getColumnName(f), value);
                }
                exporter.write(mol);
            }
            count++;
            ClientUtilities.reportProgressDetail(env, "MSG_GeneratingSDF2", count, totalRow, 100, 200);
        }
        exporter.close();
        return out.asString();
    }

    /**
     * Prepare data returned from web service for saving
     *
     * @param entity The entity
     * @param data Original data (just for comparison with returned values... to prevent unnecessary updates to DB)
     * @param sdf SDF returned from web service
     * @param env Env for reporting progress
     * @throws IOException if any problem occurs during parsing SDF, etc.
     */
    private static List<DFUpdateDescription> parseResult(DFEntity entity, Map<Comparable<?>, Map<String, Object>> data, String sdf, DFEnvironmentRO env) throws IOException {
        List<DFUpdateDescription> updates = new ArrayList<DFUpdateDescription>();
        InputStream is = new ByteArrayInputStream(sdf.getBytes("UTF-8")); // NOI18N
        MolImporter importer = new MolImporter(is, "sdf"); // NOI18N

        // Cache for used fields so it's not necessary to find them for each row.
        Map<String,DFField> fieldCache = new HashMap<String,DFField>();

        // Column where validated molecules are saved to. Create it if it doesn't exist
        DFField validatedStructureField = ClientUtilities.findField(entity, STRUCTURE_AFTER_WS_CHECK);
        if (validatedStructureField == null) {
            validatedStructureField = ClientUtilities.createNewField(entity, STRUCTURE_AFTER_WS_CHECK,
                    DBImplConstants.FIELD_STANDARD_TEXT, DBDatabaseInfo.ColumnSQLType.CLOB, env);
        }
        fieldCache.put(STRUCTURE_AFTER_WS_CHECK, validatedStructureField);

        int totalRow = data.size();
        int count = 0;

        Molecule mol = importer.read();
        while (mol != null) {
            Map<String, Object> valuesToUpdate = new HashMap<String, Object>();

            MPropertyContainer properties = mol.properties();
            Comparable<?> rowId = (Comparable<?>) entity.getIdField().getConvertor().convert(properties.getString(ClientUtilities.CD_ID), null);
            Map<String, Object> originalRow = data.get(rowId);

            for (String key: properties.getKeys()) {
                String value = properties.getString(key);
                String colName = key;
                String typeForNewField = null;

                // Cut the prefix which is saying what field type to create (for new columns)
                if (key.startsWith(TYPE_INT)) {
                    colName = key.substring(TYPE_INT.length());
                    typeForNewField = DBImplConstants.FIELD_STANDARD_INTEGER;
                }
                if (key.startsWith(TYPE_TEXT)) {
                    colName = key.substring(TYPE_TEXT.length());
                    typeForNewField = DBImplConstants.FIELD_STANDARD_TEXT;
                }

                // Find the field or create it
                DFField field = fieldCache.get(colName);
                if (field == null) {
                    field = ClientUtilities.findField(entity, colName);
                    if (field == null) {
                        field = ClientUtilities.createNewField(entity, colName, typeForNewField, null, env);
                    }
                }
                fieldCache.put(colName, field);

                // Obviously CD_ID won't be updated
                if (!colName.equals(ClientUtilities.CD_ID)) {
                    // Convert values to right type before saving to DB
                    Object convertedValue = field.getConvertor().convert(value, null);

                    // Skip it if it's equal to original value (means server didn't change it).
                    if (!Objects.equal(convertedValue, originalRow.get(field.getId()))) {
                        valuesToUpdate.put(field.getId(), convertedValue);
                    }
                }
            }
            String newMol = mol.toFormat("mol"); // NOI18N
            String oldMol = (String) originalRow.get(validatedStructureField.getId());
            // TODO: It will never be the same, because Mol contains date and time.
            // Possible improvement: is there a method which compares just molecule?
            if (!Objects.equal(newMol, oldMol)) {
                valuesToUpdate.put(validatedStructureField.getId(), newMol);
            }

            if (!valuesToUpdate.isEmpty()) {
                // Create an update descriptor for this row if there is anything to update
                DFUpdateDescription updateDescr = DFUpdateDescription.create(entity, rowId, valuesToUpdate);
                updates.add(updateDescr);
            }

            count++;
            ClientUtilities.reportProgressDetail(env, "MSG_PreparingDataForSave2", count, totalRow, 300, 400);

            mol = importer.read();
        }
        return updates;
    }

    private static void doUpdate(DFEntity entity, List<DFUpdateDescription> updates, DFEnvironmentRO env) {
        DFEntityDataProvider edp = DIFUtilities.findEntityDataProvider(entity);
        DFLock lock = edp.getLockable().obtainLock(NbBundle.getMessage(CheckStructureWSAction.class, "MSG_UpdatingData"));
        try {
            DFEnvironmentRW envRW = EnvUtils.createRWFromRO(env, lock);
            // This call will need to be split into more batches if number of checked structures will be too high
            // so they can't be hold in memory.
            edp.update(updates, DFUndoConfig.create(
                    NbBundle.getMessage(CheckStructureWSAction.class, "MSG_StructureCheck"), true), envRW);
        } finally {
            if (lock != null) {
                lock.release();
            }
        }
    }
}