com.inductiveautomation.xopc.drivers.modbus2.configuration.web.ModbusConfigurationUI.java Source code

Java tutorial

Introduction

Here is the source code for com.inductiveautomation.xopc.drivers.modbus2.configuration.web.ModbusConfigurationUI.java

Source

/*******************************************************************************
 * INDUCTIVE AUTOMATION PUBLIC LICENSE 
 *
 * BY DOWNLOADING, INSTALLING AND/OR IMPLEMENTING THIS SOFTWARE YOU AGREE 
 * TO THE FOLLOWING LICENSE: 
 *
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are 
 * met: 
 *
 * Redistributions of source code must retain the above copyright notice, 
 * this list of conditions and the following disclaimer. Redistributions in 
 * binary form must reproduce the above copyright notice, this list of 
 * conditions and the following disclaimer in the documentation and/or 
 * other materials provided with the distribution. Neither the name of 
 * Inductive Automation nor the names of its contributors may be used to 
 * endorse or promote products derived from this software without specific 
 * prior written permission. 
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 
 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 
 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL INDUCTIVE 
 * AUTOMATION BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
 * EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 *
 * LICENSEE SHALL INDEMNIFY, DEFEND AND HOLD HARMLESS INDUCTIVE AUTOMATION, 
 * ITS SHAREHOLDERS, OFFICERS, DIRECTORS, EMPLOYEES, AGENTS, ATTORNEYS, 
 * SUCCESSORS AND ASSIGNS FROM ANY AND ALL claims, debts, liabilities, 
 * demands, suits and causes of action, known or unknown, in any way 
 * relating to the LICENSEE'S USE OF THE SOFTWARE IN ANY FORM OR MANNER
 * WHATSOEVER AND FOR any act or omission related thereto.
 ******************************************************************************/
package com.inductiveautomation.xopc.drivers.modbus2.configuration.web;

import java.io.ByteArrayOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.form.OnChangeAjaxBehavior;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.Button;
import org.apache.wicket.markup.html.form.CheckBox;
import org.apache.wicket.markup.html.form.DropDownChoice;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.IChoiceRenderer;
import org.apache.wicket.markup.html.form.IFormModelUpdateListener;
import org.apache.wicket.markup.html.form.RequiredTextField;
import org.apache.wicket.markup.html.form.SubmitLink;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.markup.html.form.upload.FileUploadField;
import org.apache.wicket.markup.html.link.Link;
import org.apache.wicket.markup.html.link.ResourceLink;
import org.apache.wicket.markup.html.panel.FeedbackPanel;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.repeater.RepeatingView;
import org.apache.wicket.model.AbstractReadOnlyModel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LoadableDetachableModel;
import org.apache.wicket.model.PropertyModel;
import org.apache.wicket.request.resource.ByteArrayResource;
import org.apache.wicket.request.resource.IResource;
import org.apache.wicket.validation.IErrorMessageSource;
import org.apache.wicket.validation.IValidatable;
import org.apache.wicket.validation.IValidationError;
import org.apache.wicket.validation.IValidator;
import org.apache.wicket.validation.ValidationError;
import org.apache.wicket.validation.validator.PatternValidator;
import org.apache.wicket.validation.validator.StringValidator;

import com.inductiveautomation.ignition.common.TypeUtilities;
import com.inductiveautomation.ignition.gateway.localdb.persistence.PersistentRecord;
import com.inductiveautomation.ignition.gateway.web.components.ConfigPanel;
import com.inductiveautomation.ignition.gateway.web.models.LenientResourceModel;
import com.inductiveautomation.ignition.gateway.web.models.RecordModel;
import com.inductiveautomation.ignition.gateway.web.pages.IConfigPage;
import com.inductiveautomation.xopc.driver.api.configuration.links.ConfigurationUILink;
import com.inductiveautomation.xopc.driver.api.configuration.links.ConfigurationUILink.Callback;
import com.inductiveautomation.xopc.drivers.modbus2.configuration.map.AddressType;
import com.inductiveautomation.xopc.drivers.modbus2.configuration.map.DesignatorRange;
import com.inductiveautomation.xopc.drivers.modbus2.configuration.map.ModbusCsvParser;
import com.inductiveautomation.xopc.drivers.modbus2.configuration.map.MutableModbusAddressMap;
import com.inductiveautomation.xopc.drivers.modbus2.configuration.map.MutableModbusAddressMap.MutableDesignatorRange;
import com.inductiveautomation.xopc.drivers.modbus2.configuration.map.MutableModbusAddressMap.MutableModbusRange;
import com.inductiveautomation.xopc.drivers.modbus2.configuration.settings.ModbusDriverSettings;

public class ModbusConfigurationUI<T extends PersistentRecord & ModbusDriverSettings> extends ConfigPanel {

    private static final List<AddressType> modbusTypes = AddressType.getOptions();

    private int radix = 10;
    private MutableModbusAddressMap addressMap;
    private List<ModbusConfigurationEntry> entries = new ArrayList<ModbusConfigurationEntry>();
    private ListEditor listview;

    private final IConfigPage configPage;
    private final ConfigPanel returnPanel;
    private final Callback callback;

    public ModbusConfigurationUI(IConfigPage configPage, ConfigPanel returnPanel, Callback callback,
            T settingsRecord) {

        this.configPage = configPage;
        this.returnPanel = returnPanel;
        this.callback = callback;

        setDefaultModel(new RecordModel<PersistentRecord>(settingsRecord));

        addComponents();
    }

    @Override
    public String[] getMenuPath() {
        return new String[0];
    }

    private void addComponents() {
        T settingsRecord = (T) getDefaultModelObject();
        String mapString = (String) TypeUtilities.toString(settingsRecord.getAddressMap());
        addressMap = MutableModbusAddressMap.fromParseableString(mapString);

        if (addressMap == null) {
            addressMap = new MutableModbusAddressMap();
        }

        radix = addressMap.getDesignatorRadix();

        final Form<Object> form = new Form<Object>("form") {
            @Override
            protected void onSubmit() {
                handleOnSubmit();
            }
        };

        form.add(new FeedbackPanel("feedback"));

        final WebMarkupContainer tableContainer = new WebMarkupContainer("table-container");
        tableContainer.setOutputMarkupId(true);

        final WebMarkupContainer radixContainer = new WebMarkupContainer("radix-container") {
            @Override
            public boolean isVisible() {
                return entries.size() > 0;
            }
        };

        radixContainer.setOutputMarkupId(true);

        radixContainer.add(new Label("radix-label", new LenientResourceModel("radixlabel", "Radix")) {
            @Override
            public boolean isVisible() {
                return entries.size() > 0;
            }
        });

        final RequiredTextField<Integer> radixField = new RequiredTextField<Integer>("radix",
                new PropertyModel<Integer>(this, "radix")) {
            @Override
            public boolean isVisible() {
                return entries.size() > 0;
            }
        };

        radixField.add(new OnChangeAjaxBehavior() {
            @Override
            protected void onUpdate(AjaxRequestTarget target) {
                try {
                    Integer radix = Integer.parseInt(radixField.getValue());
                    setRadix(radix);
                } catch (Exception e) {
                }
            }
        });

        radixContainer.add(radixField);

        tableContainer.add(radixContainer);

        radixContainer.add(new Link<Object>("set-radix") {
            @Override
            public void onClick() {
                if (addressMap != null) {

                }
            }

            @Override
            public boolean isVisible() {
                return false;
            }
        });

        // Create the configuration entries for the listview
        for (DesignatorRange dr : addressMap.keySet()) {
            MutableDesignatorRange mdr = new MutableDesignatorRange(dr);
            MutableModbusRange mbr = new MutableModbusRange(addressMap.get(dr));
            entries.add(new ModbusConfigurationEntry(mdr, mbr));
        }

        // Create the listview
        listview = new ListEditor<ModbusConfigurationEntry>("config-listview", getListviewModel()) {

            @Override
            protected void onPopulateItem(ListItem<ModbusConfigurationEntry> item) {
                final ModbusConfigurationEntry configEntry = item.getModelObject();

                item.add(newPrefixTextField(configEntry));

                item.add(newStartTextField(configEntry));

                item.add(newEndTextField(configEntry));

                item.add(newStepCheckboxField(configEntry));

                item.add(newModbusUnitIDTextField(configEntry));

                item.add(newModbusAddressTypeDropdown(configEntry));

                item.add(newModbusAddressTextField(configEntry));

                item.add(new DeleteLink("delete-link"));
            }
        };

        WebMarkupContainer noMappingsContainer = new WebMarkupContainer("no-mappings-container") {
            @Override
            public boolean isVisible() {
                return entries.size() == 0;
            }

            ;
        };
        noMappingsContainer
                .add(new Label("no-mappings-label", new LenientResourceModel("nomappings", "No mappings.")));

        tableContainer.add(noMappingsContainer);

        tableContainer.add(listview);
        form.add(tableContainer);

        form.add(new SubmitLink("add-row-link") {
            {
                setDefaultFormProcessing(false);
            }

            @Override
            public void onSubmit() {
                listview.addItem(new ModbusConfigurationEntry());
            }
        });

        form.add(new Button("save"));

        add(form);

        // CSV export
        try {
            Link<IResource> exportLink = new ResourceLink<IResource>("export-link", new ExportCsvResource());
            add(exportLink);
        } catch (Exception e) {
            Link<Object> exportLink = new Link<Object>("export-link") {
                @Override
                public void onClick() {
                }

                @Override
                public boolean isVisible() {
                    return false;
                }
            };
            add(exportLink);
        }

        // CSV import
        final FileUploadField uploadField = new FileUploadField("upload-field");

        Form<?> uploadForm = new Form<Object>("upload-form") {
            @Override
            protected void onSubmit() {
                try {
                    addressMap = ModbusCsvParser.fromCsv(uploadField.getFileUpload().getInputStream());

                    radix = addressMap.getDesignatorRadix();

                    listview.clear();

                    for (DesignatorRange dr : addressMap.keySet()) {
                        MutableDesignatorRange mdr = new MutableDesignatorRange(dr);
                        MutableModbusRange mbr = new MutableModbusRange(addressMap.get(dr));
                        listview.addItem(new ModbusConfigurationEntry(mdr, mbr));
                    }

                } catch (Exception e) {
                    error("Error importing configuration from CSV file.");
                }
            }
        };

        uploadForm.add(uploadField);

        SubmitLink importLink = new SubmitLink("import-link");

        uploadForm.add(importLink);

        add(uploadForm);

    }

    private IModel<List<ModbusConfigurationEntry>> getListviewModel() {
        return new LoadableDetachableModel<List<ModbusConfigurationEntry>>() {
            @Override
            protected List<ModbusConfigurationEntry> load() {
                return entries;
            }
        };
    }

    private TextField<String> newPrefixTextField(final ModbusConfigurationEntry configEntry) {
        final RequiredTextField<String> textField = new RequiredTextField<String>("prefix",
                new PropertyModel<String>(configEntry, "designatorRange.designator"));

        textField.add(new PatternValidator("[A-Za-z0-9_]+") {
            @Override
            protected ValidationError decorate(ValidationError error, IValidatable<String> validatable) {
                error.addKey("prefix.PatternValidator");
                return error;
            }
        });

        textField.add(new StringValidator(1, 12) {
            @Override
            protected ValidationError decorate(ValidationError error, IValidatable<String> validatable) {
                error.addKey("prefix.LengthValidator");
                return error;
            }
        });

        textField.add(new PrefixValidator());

        textField.add(new OnChangeAjaxBehavior() {
            @Override
            protected void onUpdate(AjaxRequestTarget target) {
                configEntry.getDesignatorRange().setDesignator(textField.getModelObject());
            }
        });

        return textField;
    }

    static class PrefixValidator extends StringValidator {
        @Override
        public void validate(IValidatable<String> validatable) {
            final String value = validatable.getValue();

            if (StringUtils.equalsIgnoreCase("HR", value) || StringUtils.equalsIgnoreCase("IR", value)
                    || StringUtils.equalsIgnoreCase("C", value) || StringUtils.equalsIgnoreCase("DI", value)) {
                class PrefixError implements IValidationError, Serializable {
                    @Override
                    public String getErrorMessage(IErrorMessageSource arg0) {
                        return String.format("Prefix \"%s\" not allowed.", value);
                    }
                }
                validatable.error(new PrefixError());
            }
        }
    }

    private TextField<String> newStartTextField(final ModbusConfigurationEntry configEntry) {
        final RequiredTextField<String> textField = new RequiredTextField<String>("start",
                new PropertyModel<String>(configEntry, "designatorRange.start"));

        textField.add(new RadixValidator());

        textField.add(new OnChangeAjaxBehavior() {
            @Override
            protected void onUpdate(AjaxRequestTarget target) {
                configEntry.getDesignatorRange().setStart(textField.getModelObject());
            }
        });

        return textField;
    }

    private TextField<String> newEndTextField(final ModbusConfigurationEntry configEntry) {
        final RequiredTextField<String> textField = new RequiredTextField<String>("end",
                new PropertyModel<String>(configEntry, "designatorRange.end"));

        textField.add(new RadixValidator());

        textField.add(new OnChangeAjaxBehavior() {
            @Override
            protected void onUpdate(AjaxRequestTarget target) {
                configEntry.getDesignatorRange().setEnd(textField.getModelObject());
            }
        });

        return textField;
    }

    private CheckBox newStepCheckboxField(final ModbusConfigurationEntry configEntry) {
        final CheckBox checkboxField = new CheckBox("step",
                new PropertyModel<Boolean>(configEntry, "designatorRange.step"));

        checkboxField.add(new OnChangeAjaxBehavior() {
            @Override
            protected void onUpdate(AjaxRequestTarget target) {
                configEntry.getDesignatorRange().setStep(checkboxField.getModelObject());
            }
        });

        return checkboxField;
    }

    private TextField<String> newModbusUnitIDTextField(final ModbusConfigurationEntry configEntry) {
        final RequiredTextField<String> textField = new RequiredTextField<String>("unitid",
                new PropertyModel<String>(configEntry, "modbusRange.unitID"));

        textField.add(new ModbusUnitIDValidator());

        textField.add(new OnChangeAjaxBehavior() {
            @Override
            protected void onUpdate(AjaxRequestTarget target) {
                configEntry.modbusRange.setUnitID(textField.getModelObject());
            }
        });

        return textField;
    }

    private TextField<String> newModbusAddressTextField(final ModbusConfigurationEntry configEntry) {
        final RequiredTextField<String> textField = new RequiredTextField<String>("address",
                new PropertyModel<String>(configEntry, "modbusRange.start"));

        textField.add(new ModbusAddressValidator());

        textField.add(new OnChangeAjaxBehavior() {
            @Override
            protected void onUpdate(AjaxRequestTarget target) {
                configEntry.modbusRange.setStart(textField.getModelObject());
            }
        });

        return textField;
    }

    private DropDownChoice<AddressType> newModbusAddressTypeDropdown(final ModbusConfigurationEntry configEntry) {
        final DropDownChoice<AddressType> dropDown = new DropDownChoice<AddressType>("type",
                new PropertyModel<AddressType>(configEntry, "modbusRange.modbusAddressType"), modbusTypes,
                new ModbusAddressTypeChoiceRenderer());

        dropDown.setRequired(true);

        dropDown.add(new OnChangeAjaxBehavior() {
            @Override
            protected void onUpdate(AjaxRequestTarget target) {
                configEntry.getModbusRange().setModbusAddressType(dropDown.getModelObject());
            }
        });

        return dropDown;
    }

    public int getRadix() {
        return radix;
    }

    public void setRadix(int radix) {
        this.radix = radix;
    }

    private void handleOnSubmit() {
        MutableModbusAddressMap addressMap = new MutableModbusAddressMap();
        entries = (List<ModbusConfigurationEntry>) listview.getDefaultModelObject();

        for (ModbusConfigurationEntry entry : entries) {
            MutableDesignatorRange dr = entry.getDesignatorRange();
            MutableModbusRange mr = entry.getModbusRange();

            addressMap.put(dr, mr);
        }

        addressMap.setDesignatorRadix(radix);

        T settingsRecord = (T) getDefaultModelObject();
        settingsRecord.setAddressMap(addressMap.toParseableString());
        callback.save(settingsRecord);
        configPage.setConfigPanel(returnPanel);
    }

    @Override
    public IModel<String> getTitleModel() {
        return new LenientResourceModel("configurationtitle", "Address Configuration");
    }

    private class ModbusAddressTypeChoiceRenderer implements IChoiceRenderer<AddressType> {
        @Override
        public Object getDisplayValue(AddressType type) {
            return type.getDisplayString();
        }

        @Override
        public String getIdValue(AddressType type, int index) {
            return String.valueOf(index);
        }
    }

    public static class ModbusConfigurationEntry implements Serializable {

        private MutableDesignatorRange designatorRange;
        private MutableModbusRange modbusRange;

        public ModbusConfigurationEntry() {
            designatorRange = new MutableDesignatorRange();
            modbusRange = new MutableModbusRange();
        }

        public ModbusConfigurationEntry(MutableDesignatorRange designatorRange, MutableModbusRange modbusRange) {
            this.designatorRange = designatorRange;
            this.modbusRange = modbusRange;
        }

        public MutableDesignatorRange getDesignatorRange() {
            return designatorRange;
        }

        public MutableModbusRange getModbusRange() {
            return modbusRange;
        }

    }

    private class RadixValidator implements IValidator<String>, Serializable {

        @Override
        public void validate(IValidatable<String> validatable) {
            String value = validatable.getValue();

            try {
                Integer.parseInt(value, radix);
            } catch (Exception e) {
                validatable.error(new RadixError(value, radix));
            }
        }

        private class RadixError implements IValidationError, Serializable {
            private String value;
            private int base;

            public RadixError(String value, int base) {
                this.value = value;
                this.base = base;
            }

            @Override
            public String getErrorMessage(IErrorMessageSource source) {
                return String.format("Value \"%s\" outside of specified number base \"%s\".", value, base);
            }
        }
    }

    private class ModbusUnitIDValidator implements IValidator<String>, Serializable {

        @Override
        public void validate(IValidatable<String> validatable) {
            String value = validatable.getValue();
            try {
                int unitID = Integer.parseInt(value);
                if (unitID < 0 || unitID > 255) {
                    validatable.error(new ModbusUnitIDError());
                }
            } catch (Exception e) {
                validatable.error(new ModbusUnitIDError());
            }
        }

        private class ModbusUnitIDError implements IValidationError, Serializable {
            @Override
            public String getErrorMessage(IErrorMessageSource arg0) {
                return "Modbus Unit ID must be blank or a number between 0 and 255";
            }
        }

    }

    private class ModbusAddressValidator implements IValidator<String>, Serializable {

        @Override
        public void validate(IValidatable<String> validatable) {
            String value = validatable.getValue();
            try {
                int address = Integer.parseInt(value);
                if (address < 0 || address > 65535) {
                    validatable.error(new ModbusError());
                }
            } catch (Exception e) {
                validatable.error(new ModbusError());
            }
        }

        private class ModbusError implements IValidationError, Serializable {
            @Override
            public String getErrorMessage(IErrorMessageSource arg0) {
                return "Modbus Address must be between 0 and 65535";
            }
        }

    }

    private class ExportCsvResource extends ByteArrayResource {

        private byte[] resource;

        ExportCsvResource() {
            super("text/plain", null, "modbus-config.csv");
        }

        @Override
        protected byte[] getData(Attributes attributes) {
            MutableModbusAddressMap addressMap = new MutableModbusAddressMap();

            for (ModbusConfigurationEntry entry : entries) {
                MutableDesignatorRange dr = entry.getDesignatorRange();
                MutableModbusRange mr = entry.getModbusRange();

                addressMap.put(dr, mr);
            }

            addressMap.setDesignatorRadix(radix);

            ByteArrayOutputStream baos = new ByteArrayOutputStream();

            try {
                addressMap.toCsv(baos);
            } catch (Exception e) {
            }

            resource = baos.toByteArray();

            IOUtils.closeQuietly(baos);
            return resource;
        }

    }

    public abstract class ListEditor<T> extends RepeatingView implements IFormModelUpdateListener {
        List<T> items;

        public ListEditor(String id, IModel<List<T>> model) {
            super(id, model);
        }

        protected abstract void onPopulateItem(ListItem<T> item);

        public void addItem(T value) {
            items.add(value);
            ListItem<T> item = new ListItem<T>(newChildId(), items.size() - 1);
            add(item);
            onPopulateItem(item);
        }

        public void clear() {
            items.clear();
            removeAll();
        }

        protected void onBeforeRender() {
            if (!hasBeenRendered()) {
                items = new ArrayList<T>((List<T>) getDefaultModelObject());
                for (int i = 0; i < items.size(); i++) {
                    ListItem<T> li = new ListItem<T>(newChildId(), i);
                    add(li);
                    onPopulateItem(li);
                }
            }
            super.onBeforeRender();
        }

        public void updateModel() {
            setDefaultModelObject(items);
        }
    }

    public class ListItem<T> extends Item<T> {
        public ListItem(String id, int index) {
            super(id, index);
            setModel(new ListItemModel());
        }

        private class ListItemModel extends AbstractReadOnlyModel<T> {
            public T getObject() {
                return ((ListEditor<T>) ListItem.this.getParent()).items.get(getIndex());
            }
        }
    }

    public abstract class EditorLink extends SubmitLink {
        private transient ListItem<?> parent;

        public EditorLink(String id) {
            super(id);
        }

        protected final ListItem<?> getItem() {
            if (parent == null) {
                parent = findParent(ListItem.class);
            }
            return parent;
        }

        protected final List<?> getList() {
            return getEditor().items;
        }

        protected final ListEditor<?> getEditor() {
            return (ListEditor<?>) getItem().getParent();
        }

        protected void onDetach() {
            parent = null;
            super.onDetach();
        }

    }

    public class DeleteLink extends EditorLink {

        public DeleteLink(String id) {
            super(id);
            setDefaultFormProcessing(false);
        }

        @Override
        public void onSubmit() {
            int idx = getItem().getIndex();

            for (int i = idx + 1; i < getItem().getParent().size(); i++) {
                ListItem<?> item = (ListItem<?>) getItem().getParent().get(i);
                item.setIndex(item.getIndex() - 1);
            }

            getList().remove(idx);
            getEditor().remove(getItem());
        }
    }

}