IbisEditFeatureActionBean.java

/*
 * Copyright (C) 2015 B3Partners B.V.
 *
 * 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/>.
 */
package nl.b3p.viewer.stripes;

import net.sourceforge.stripes.validation.Validate;
import nl.b3p.viewer.ibis.util.IbisConstants;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.io.WKTReader;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.NoSuchElementException;
import javax.persistence.EntityManager;
import net.sourceforge.stripes.action.StrictBinding;
import net.sourceforge.stripes.action.UrlBinding;
import nl.b3p.viewer.config.app.ApplicationLayer;
import nl.b3p.viewer.config.services.Layer;
import nl.b3p.viewer.ibis.util.WorkflowStatus;
import nl.b3p.viewer.ibis.util.WorkflowUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.geotools.data.DataUtilities;
import org.geotools.data.DefaultTransaction;
import org.geotools.data.Transaction;
import org.geotools.data.simple.SimpleFeatureStore;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.filter.identity.FeatureIdImpl;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.feature.type.GeometryType;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory2;
import org.opengis.filter.sort.SortOrder;
import org.stripesstuff.stripersist.Stripersist;

/**
 * edit component met ibis workflow.
 *
 * @author mprins
 */
@UrlBinding("/action/feature/ibisedit")
@StrictBinding
public class IbisEditFeatureActionBean extends EditFeatureActionBean implements IbisConstants {

    private static final Log log = LogFactory.getLog(IbisEditFeatureActionBean.class);

    @Validate
    private boolean historisch;

    @Validate
    private boolean reject;

    @Override
    protected String addNewFeature() throws Exception {
        String kavelID = super.addNewFeature();
        //update  terrein
        Object terreinID = this.getJsonFeature().optString(KAVEL_TERREIN_ID_FIELDNAME, null);
        WorkflowStatus status = WorkflowStatus.valueOf(this.getJsonFeature().optString(WORKFLOW_FIELDNAME, WorkflowStatus.bewerkt.name()));
        if (terreinID != null) {
            WorkflowUtil.updateTerreinGeometry(Integer.parseInt(terreinID.toString()), this.getLayer(), status, this.getApplication(), Stripersist.getEntityManager());
        }
        return kavelID;
    }

    /**
     * Override to not delete a feature but set workflow status to
     * {@code WorkflowStatus.archief}
     *
     * @param fid feature id
     * @throws IOException if any
     * @throws Exception if any
     * @see WorkflowStatus
     */
    @Override
    protected void deleteFeature(String fid) throws IOException, Exception {
        if(this.isHistorisch()){
            this.deleteFeatureHistorisch(fid);
        } else if (this.isReject()){
            this.rejectFeature(fid);
        } else{
            log.debug("ibis deleteFeature: " + fid);

            Transaction transaction = new DefaultTransaction("ibis_delete");
            this.getStore().setTransaction(transaction);

            FilterFactory2 ff = CommonFactoryFinder.getFilterFactory2();
            Filter filter = ff.id(new FeatureIdImpl(fid));

            try {
                this.getStore().modifyFeatures(WORKFLOW_FIELDNAME, WorkflowStatus.afgevoerd, filter);
                SimpleFeature original = this.getStore().getFeatures(filter).features().next();
                transaction.commit();
                Object terreinID = original.getAttribute(KAVEL_TERREIN_ID_FIELDNAME);
                if (terreinID != null) {
                    WorkflowUtil.updateTerreinGeometry(Integer.parseInt(terreinID.toString()), this.getLayer(), WorkflowStatus.afgevoerd, this.getApplication(), Stripersist.getEntityManager());
                }
            } catch (Exception e) {
                transaction.rollback();
                throw e;
            } finally {
                transaction.close();
            }
        }
    }

    /**
     * verwijder historische record.
     * Als het een "archief" record is dan gewoon verwijderen,
     * als het een "afgevoerd" record is dan verwijderen en jongste "archief" record "afgevoerd" maken.
     * als er geen "archief" te vinden is dan laten.
     *
     * @param fid feature to remove
     * @throws IOException if any
     * @throws Exception if any
     */
    private void deleteFeatureHistorisch(String fid) throws IOException, Exception {
        log.debug("ibis deleteFeatureHistorisch: " + fid);

        Transaction transaction = new DefaultTransaction("ibis_delete_historisch");
        this.getStore().setTransaction(transaction);

        FilterFactory2 ff = CommonFactoryFinder.getFilterFactory2();
        Filter fidFilter = ff.id(new FeatureIdImpl(fid));

        Filter hist = ff.or(
                ff.equal(ff.property(WORKFLOW_FIELDNAME), ff.literal(WorkflowStatus.archief.name()), false),
                ff.equal(ff.property(WORKFLOW_FIELDNAME), ff.literal(WorkflowStatus.afgevoerd.name()), false)
        );

        try {
            SimpleFeature original = this.getStore().getFeatures(fidFilter).features().next();
            Number ibisId = (Number) original.getAttribute(ID_FIELDNAME);
            Object terreinID = original.getAttribute(KAVEL_TERREIN_ID_FIELDNAME);

            log.debug(String.format("Delete from feature source #%d fid=%s",
                    this.getLayer().getFeatureType().getId(), fid));

            switch (WorkflowStatus.valueOf( original.getAttribute(WORKFLOW_FIELDNAME).toString()) ) {
                case archief:
                    // archief record gewoon verwijderen
                    this.getStore().removeFeatures(fidFilter);
                    break;
                case afgevoerd:
                    // zoek alle historische kavels voor dit kavel nummer
                    SimpleFeatureCollection histRecords = this.getStore().getFeatures(
                            ff.and(
                                    hist,
                                    ff.equals(ff.property(ID_FIELDNAME), ff.literal(ibisId))
                            ));
                    if (histRecords.size() > 1) {
                        // als er naast deze afgevoerd nog andere zijn
                        histRecords.sort(ff.sort(MUTATIEDATUM_FIELDNAME, SortOrder.DESCENDING));
                        // dan de jongste daarvan (eerste uit de lijst) op afgevoerd zetten
                        String updateFid = histRecords.features().next().getID();
                        this.getStore().modifyFeatures(WORKFLOW_FIELDNAME, WorkflowStatus.afgevoerd, ff.id(new FeatureIdImpl(updateFid)));
                        // en gevraagde verwijderen uit database
                        this.getStore().removeFeatures(fidFilter);
                    }
                    break;
                default:
                    throw new UnsupportedOperationException("Historische record met status anders dan 'archief' of 'afgevoerd' kan niet worden verwijderd.");
            }
            transaction.commit();
            // update terrein van kavel
            if (terreinID != null) {
                WorkflowUtil.updateTerreinGeometry(Integer.parseInt(terreinID.toString()), this.getLayer(), WorkflowStatus.afgevoerd, this.getApplication(), Stripersist.getEntityManager());
            }
        } catch (Exception e) {
            transaction.rollback();
            throw e;
        } finally {
            transaction.close();
        }

    }

    /** voor "afkeur" van een bewerkt kavel.
     *
     * @param fid feature to remove
     * @throws Exception if any
     */
    private void rejectFeature(String fid) throws Exception {
        log.debug("ibis rejectFeature: " + fid);

        Transaction transaction = new DefaultTransaction("ibis_reject");
        this.getStore().setTransaction(transaction);
        Filter filter = CommonFactoryFinder.getFilterFactory2().id(new FeatureIdImpl(fid));

        try {
            SimpleFeature original = this.getStore().getFeatures(filter).features().next();
            if (!(original.getAttribute(WORKFLOW_FIELDNAME).toString().equalsIgnoreCase(WorkflowStatus.bewerkt.label()) &&
                    (this.getLayer().getName().equalsIgnoreCase(KAVEL_LAYER_NAME) || this.getLayer().getName().equalsIgnoreCase(TERREIN_LAYER_NAME)))) {
                throw new IllegalArgumentException("Alleen 'bewerkt' kavels en terreinen kunnen worden afgekeurd.");
            }
            Object terreinID = original.getAttribute(KAVEL_TERREIN_ID_FIELDNAME);
            this.getStore().removeFeatures(filter);
            transaction.commit();

            if (terreinID != null) {
                WorkflowUtil.updateTerreinGeometry(Integer.parseInt(terreinID.toString()), this.getLayer(), WorkflowStatus.afgevoerd, this.getApplication(), Stripersist.getEntityManager());
            }
        } catch (Exception e) {
            transaction.rollback();
            throw e;
        } finally {
            transaction.close();
        }
    }
    /**
     * Override the method from the base class to process our workflow.
     *
     * @param fid feature id
     * @throws Exception if any
     */
    @Override
    protected void editFeature(String fid) throws Exception {
        log.debug("ibis editFeature:" + fid);
        FilterFactory2 ff = CommonFactoryFinder.getFilterFactory2();
        Filter fidFilter = ff.id(new FeatureIdImpl(fid));

        List<String> attributes = new ArrayList();
        List values = new ArrayList();
        WorkflowStatus incomingWorkflowStatus = null;
        // parse json
        for (Iterator<String> it = this.getJsonFeature().keys(); it.hasNext();) {
            String attribute = it.next();
            if (!this.getFID().equals(attribute)) {
                AttributeDescriptor ad = this.getStore().getSchema().getDescriptor(attribute);
                if (ad != null) {
                    if (!isAttributeUserEditingDisabled(attribute)) {
                        attributes.add(attribute);
                        if (ad.getType() instanceof GeometryType) {
                            String wkt = this.getJsonFeature().getString(ad.getLocalName());
                            Geometry g = null;
                            if (wkt != null) {
                                g = new WKTReader().read(wkt);
                            }
                            values.add(g);
                        } else {
                            String v = this.getJsonFeature().getString(attribute);
                            values.add(StringUtils.defaultIfBlank(v, null));
                            // remember the incoming workflow status
                            if (attribute.equals(WORKFLOW_FIELDNAME)) {
                                incomingWorkflowStatus = WorkflowStatus.valueOf(v);
                            }
                        }
                    } else {
                        log.info(String.format("Attribute \"%s\" not user editable; ignoring", attribute));
                    }
                } else {
                    log.warn(String.format("Attribute \"%s\" not in feature type; ignoring", attribute));
                }
            }
        }
        log.debug(String.format("Modifying feature source #%d fid=%s, attributes=%s, values=%s",
                this.getLayer().getFeatureType().getId(), fid, attributes.toString(), values.toString()));

        Transaction editTransaction = new DefaultTransaction("ibis_edit");
        this.getStore().setTransaction(editTransaction);
        try {
            if (incomingWorkflowStatus == null) {
                throw new IllegalArgumentException("Workflow status van edit feature is null, dit wordt niet ondersteund.");
            }
            SimpleFeature original = this.getStore().getFeatures(fidFilter).features().next();
            Object terreinID = original.getAttribute(KAVEL_TERREIN_ID_FIELDNAME);
            WorkflowStatus originalWorkflowStatus = WorkflowStatus.valueOf(original.getAttribute(WORKFLOW_FIELDNAME).toString());

            // make a copy of the original feature and set (new) attribute values on the copy
            SimpleFeature editedNewFeature = createCopy(original);
            for (int i = 0; i < attributes.size(); i++) {
                editedNewFeature.setAttribute(attributes.get(i), values.get(i));
            }

            Filter definitief = ff.and(
                    ff.equals(ff.property(ID_FIELDNAME), ff.literal(original.getAttribute(ID_FIELDNAME))),
                    ff.equal(ff.property(WORKFLOW_FIELDNAME), ff.literal(WorkflowStatus.definitief.name()), false)
            );
            boolean definitiefExists = (this.getStore().getFeatures(definitief).size() > 0);

            Filter bewerkt = ff.and(
                    ff.equals(ff.property(ID_FIELDNAME), ff.literal(original.getAttribute(ID_FIELDNAME))),
                    ff.equal(ff.property(WORKFLOW_FIELDNAME), ff.literal(WorkflowStatus.bewerkt.name()), false)
            );
            int aantalBewerkt = this.getStore().getFeatures(bewerkt).size();
            boolean bewerktExists = (aantalBewerkt > 0);

            switch (incomingWorkflowStatus) {
                case bewerkt:
                    if (originalWorkflowStatus.equals(WorkflowStatus.definitief)) {
                        // definitief -> bewerkt
                        if (bewerktExists) {
                            // delete existing bewerkt
                            this.getStore().removeFeatures(bewerkt);
                        }
                        // insert new record with original id and workflowstatus "bewerkt", leave original "definitief"
                        this.getStore().addFeatures(DataUtilities.collection(editedNewFeature));
                    } else if (originalWorkflowStatus.equals(WorkflowStatus.bewerkt)) {
                        // bewerkt -> bewerkt, overwrite, only one 'bewerkt' is allowed
                        if (aantalBewerkt > 1) {
                            log.error("Er is meer dan 1 bewerkt kavel/terrein voor " + ID_FIELDNAME + "=" + original.getAttribute(ID_FIELDNAME));
                            // more than 1 bewerkt, move them to archief
                            this.getStore().modifyFeatures(WORKFLOW_FIELDNAME, WorkflowStatus.archief, bewerkt);
                        }
                        this.getStore().modifyFeatures(attributes.toArray(new String[attributes.size()]), values.toArray(), fidFilter);
                    } else {
                        // other behaviour not documented eg. archief -> bewerkt, afgevoerd -> bewerkt
                        //  and not possible to occur in the application as only definitief and bewerkt can be edited
                        throw new IllegalArgumentException(String.format(
                                "Niet ondersteunde workflow stap van %s naar %s",
                                originalWorkflowStatus.label(),
                                incomingWorkflowStatus.label()));
                    }
                    break;
                case definitief:
                    if (definitiefExists) {
                        // check if any "definitief" exists for this id and move that to "archief"
                        this.getStore().modifyFeatures(WORKFLOW_FIELDNAME, WorkflowStatus.archief, definitief);
                    }
                    if (originalWorkflowStatus.equals(WorkflowStatus.definitief)) {
                        // if the original was "definitief" insert a new "definitief"
                        this.getStore().addFeatures(DataUtilities.collection(editedNewFeature));
                    } else if (originalWorkflowStatus.equals(WorkflowStatus.bewerkt)) {
                        // if original was "bewerkt" update this to "definitief" with the edits
                        this.getStore().modifyFeatures(attributes.toArray(new String[attributes.size()]), values.toArray(), fidFilter);
                    } else {
                        // other behaviour not documented eg. archief -> definitief, afgevoerd -> definitief
                        //  and not possible to occur in the application as only definitief and bewerkt can be edited
                        throw new IllegalArgumentException(String.format(
                                "Niet ondersteunde workflow stap van %s naar %s",
                                originalWorkflowStatus.label(),
                                incomingWorkflowStatus.label()));
                    }
                    break;
                case afgevoerd:
                    if (definitiefExists) {
                        // update any "definitief" for this id to "archief"
                        this.getStore().modifyFeatures(WORKFLOW_FIELDNAME, WorkflowStatus.archief, definitief);
                    }
                    // update original with the new/edited data including "afgevoerd"
                    this.getStore().modifyFeatures(attributes.toArray(new String[attributes.size()]), values.toArray(), fidFilter);

                    if (terreinID == null) {
                        // find any kavels related to this terrein and also set them to "afgevoerd"
                        Filter kavelFilter = ff.equals(ff.property(KAVEL_TERREIN_ID_FIELDNAME), ff.literal(original.getAttribute(ID_FIELDNAME)));
                        this.updateKavelWorkflowForTerrein(kavelFilter, WorkflowStatus.afgevoerd);
                    }
                    break;
                case archief: {
                    // not described, for now just edit the feature
                    this.getStore().modifyFeatures(attributes.toArray(new String[attributes.size()]), values.toArray(), fidFilter);
                    break;
                }
                default:
                    throw new IllegalArgumentException("Workflow status van edit feature is null, dit wordt niet ondersteund.");
            }

            editTransaction.commit();
            editTransaction.close();
            // update terrein geometry
            if (terreinID != null) {
                WorkflowUtil.updateTerreinGeometry(Integer.parseInt(terreinID.toString()), this.getLayer(), incomingWorkflowStatus, this.getApplication(), Stripersist.getEntityManager());
            }
        } catch (IllegalArgumentException | IOException | NoSuchElementException e) {
            editTransaction.rollback();
            log.error("Ibis editFeature error", e);
            throw e;
        }
    }

    /**
     * Make a copy of the original, but with a new fid.
     *
     * @param copyFrom original
     * @return copy having same attribute values as original and a new fid
     */
    private SimpleFeature createCopy(SimpleFeature copyFrom) {
        SimpleFeatureBuilder builder = new SimpleFeatureBuilder(copyFrom.getFeatureType());
        builder.init(copyFrom);
        return builder.buildFeature(null);
    }

    /**
     * Update any of the selected kavels to the given workflow.
     *
     * @param kavelFilter filter definitie
     * @param newStatus de nieuwe status
     */
    private void updateKavelWorkflowForTerrein(Filter kavelFilter, WorkflowStatus newStatus) {
        SimpleFeatureStore kavelStore = null;
        try (Transaction kavelTransaction = new DefaultTransaction("get-related-kavel-geom")) {
            EntityManager em = Stripersist.getEntityManager();
            log.debug("updating kavels voor terrein met filter: " + kavelFilter);
            // find kavel appLayer
            ApplicationLayer kavelAppLyr = null;
            List<ApplicationLayer> lyrs = this.getApplication().loadTreeCache(em).getApplicationLayers();
            for (ListIterator<ApplicationLayer> it = lyrs.listIterator(); it.hasNext();) {
                kavelAppLyr = it.next();
                if (kavelAppLyr.getLayerName().equalsIgnoreCase(KAVEL_LAYER_NAME)) {
                    break;
                }
            }
            Layer l = kavelAppLyr.getService().getLayer(KAVEL_LAYER_NAME, em);
            kavelStore = (SimpleFeatureStore) l.getFeatureType().openGeoToolsFeatureSource();
            kavelStore.setTransaction(kavelTransaction);
            // update kavels
            kavelStore.modifyFeatures(WORKFLOW_FIELDNAME, newStatus, kavelFilter);
            kavelTransaction.commit();
            kavelTransaction.close();
        } catch (Exception e) {
            log.error(String.format("Bijwerken van kavel workflow status naar %s voor terrein met %s is mislukt.",
                    newStatus, kavelFilter), e);
        } finally {
            if (kavelStore != null) {
                kavelStore.getDataStore().dispose();
            }
        }
    }

    /**
     * Check that if {@code disableUserEdit} flag is set on the attribute.
     * Override superclass behaviour for the workflow field, so that it's not
     * editable client side, but it can be set programatically in the client.
     *
     * @param attrName attribute to check
     * @return {@code true} when the configured attribute is flagged as
     * "readOnly" except when this is workflow status
     */
    @Override
    protected boolean isAttributeUserEditingDisabled(String attrName) {
        boolean isAttributeUserEditingDisabled = super.isAttributeUserEditingDisabled(attrName);

        if (attrName.equalsIgnoreCase(WORKFLOW_FIELDNAME)) {
            isAttributeUserEditingDisabled = false;
        }

        return isAttributeUserEditingDisabled;
    }

    private boolean isSameMutatiedatum(Object datum1, Object datum2) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-mm-dd");
        return sdf.format(datum1).equals(sdf.format(datum2));
    }

    public boolean isHistorisch() {
        return historisch;
    }

    public void setHistorisch(boolean historisch) {
        this.historisch = historisch;
    }

    public boolean isReject() {
        return reject;
    }

    public void setReject(boolean reject) {
        this.reject = reject;
    }
}