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;
}
}