package com.revolsys.swing.map.layer.record.component; import java.awt.Color; import java.awt.Dimension; import java.util.Arrays; import java.util.List; import java.util.function.BiFunction; import javax.script.Bindings; import javax.script.Compilable; import javax.script.CompiledScript; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.ScriptException; import javax.swing.BorderFactory; import javax.swing.JComponent; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextArea; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.text.BadLocationException; import javax.swing.text.Document; import org.jdesktop.swingx.VerticalLayout; import com.revolsys.awt.WebColors; import com.revolsys.identifier.Identifier; import com.revolsys.record.ArrayRecord; import com.revolsys.record.Record; import com.revolsys.record.code.CodeTable; import com.revolsys.record.schema.FieldDefinition; import com.revolsys.record.schema.RecordDefinition; import com.revolsys.swing.action.enablecheck.EnableCheck; import com.revolsys.swing.field.ComboBox; import com.revolsys.swing.field.TextArea; import com.revolsys.swing.map.layer.record.AbstractRecordLayer; import com.revolsys.swing.map.layer.record.LayerRecord; import com.revolsys.swing.menu.MenuFactory; import com.revolsys.swing.toolbar.ToolBar; import com.revolsys.util.Exceptions; import com.revolsys.util.Property; public class FieldCalculator extends AbstractUpdateField implements DocumentListener { private static final long serialVersionUID = 1L; public static void addMenuItem(final MenuFactory headerMenu) { final EnableCheck enableCheck = newEnableCheck(); headerMenu.addMenuItem("field", "Field Calculator", "calculator_edit", enableCheck, FieldCalculator::showDialog); } private static void showDialog() { final FieldCalculator dialog = new FieldCalculator(); dialog.setVisible(true); } private JTextArea expressionField; private final ScriptEngine scriptEngine = new ScriptEngineManager().getEngineByName("nashorn"); private final Compilable scriptEngineCompiler = (Compilable)this.scriptEngine; private CompiledScript script; private final BiFunction<String, Object, Object> codeIdFunction = (fieldName, value) -> { final AbstractRecordLayer layer = getLayer(); final RecordDefinition recordDefinition = layer.getRecordDefinition(); final CodeTable codeTable = recordDefinition.getCodeTableByFieldName(fieldName); if (codeTable != null) { final Identifier id = codeTable.getIdentifier(value); if (id == null) { return null; } else { final List<Object> values = id.getValues(); if (values.size() == 1) { return values.get(0); } return values; } } return value; }; private final BiFunction<String, Object, Object> codeValueFunction = (fieldName, id) -> { final AbstractRecordLayer layer = getLayer(); final RecordDefinition recordDefinition = layer.getRecordDefinition(); final CodeTable codeTable = recordDefinition.getCodeTableByFieldName(fieldName); if (codeTable != null) { final Object value = codeTable.getValue(id); return value; } return id; }; private TextArea errorsField; private FieldCalculator() { super("Field Calculator"); } private void addTextButton(final String groupName, final ToolBar toolBar, final String label, final String text) { final Runnable action = () -> { insertText(text); }; toolBar.addButton(groupName, label, action) // .setBorderPainted(true); } @Override public void changedUpdate(final DocumentEvent e) { validateExpression(); } @Override protected String getProgressMonitorTitle() { return "Calculating " + this.getFieldDefinition().getName() + " values"; } @Override protected JComponent initErrorsPanel() { final Color background = new JPanel().getBackground(); this.errorsField = new TextArea("errors", 5, 60); this.errorsField.setEditable(false); this.errorsField.setForeground(WebColors.Red); this.errorsField.setBackground(background); final JScrollPane errorScroll = new JScrollPane(this.errorsField); errorScroll.setBorder(BorderFactory.createTitledBorder("Errors")); errorScroll.setBackground(background); return errorScroll; } @Override protected JPanel initFieldPanel() { final FieldDefinition fieldDefinition = this.getFieldDefinition(); final String fieldName = fieldDefinition.getName(); final JPanel fieldPanel = new JPanel(new VerticalLayout()); final ToolBar toolBar = new ToolBar(); fieldPanel.add(toolBar); this.expressionField = new TextArea("script", 8, 50); fieldPanel.add(new JScrollPane(this.expressionField)); this.expressionField.getDocument().addDocumentListener(this); final AbstractRecordLayer layer = getLayer(); final List<String> fieldNames = layer.getFieldNames(); final ComboBox<String> fieldNamesField = ComboBox.newComboBox("fieldNames", fieldNames, (final Object name) -> { return layer.getFieldTitle((String)name); }); toolBar.addComponent("fieldName", fieldNamesField); toolBar.add(fieldNamesField); fieldNamesField.setMaximumSize(new Dimension(250, 30)); final Runnable addFieldAction = () -> { final String selectedFieldName = fieldNamesField.getFieldValue(); insertText(selectedFieldName); }; toolBar.addButton("fieldName", "Add field name", "add", addFieldAction); for (final String text : Arrays.asList("+", "-", "*", "/")) { addTextButton("operators", toolBar, text, text); } addTextButton("condition", toolBar, "if", "if (expression) {\n newValue;\n} else {\n " + fieldName + ";\n}"); addTextButton("codeTable", toolBar, "Code ID", "codeId('codeFieldName', codeValue)"); addTextButton("codeTable", toolBar, "Code Value", "codeValue('codeFieldName', codeValue)"); return fieldPanel; } private void insertText(final String text) { int location = this.expressionField.getCaretPosition(); final Document document = this.expressionField.getDocument(); boolean needsWhitespaceNext = false; try { // Remove selected text final int selectionStart = this.expressionField.getSelectionStart(); final int selectionEnd = this.expressionField.getSelectionEnd(); final int selectionLength = selectionEnd - selectionStart; if (selectionLength > 0) { document.remove(selectionStart, selectionLength); } if (location > 0 && !Character.isWhitespace(document.getText(location - 1, 1).charAt(0))) { this.expressionField.insert(" ", location); location++; } final int newLocation = location + text.length(); if (location < document.getLength() - 1 && !Character.isWhitespace(document.getText(location + 1, 1).charAt(0))) { needsWhitespaceNext = true; } this.expressionField.insert(text, location); if (needsWhitespaceNext) { this.expressionField.insert(" ", newLocation); } } catch (final BadLocationException e) { this.expressionField.append(text); } } @Override public void insertUpdate(final DocumentEvent e) { validateExpression(); } protected Bindings newBindings() { final Bindings bindings = this.scriptEngine.createBindings(); bindings.put("codeId", this.codeIdFunction); bindings.put("codeValue", this.codeValueFunction); return bindings; } @Override public void removeUpdate(final DocumentEvent e) { validateExpression(); } @Override protected void updateRecord(final LayerRecord record) { final Bindings bindings = newBindings(); bindings.putAll(record); bindings.put("record", new ArrayRecord(record)); try { final FieldDefinition fieldDefinition = getFieldDefinition(); final String fieldName = fieldDefinition.getName(); Object value = this.script.eval(bindings); value = fieldDefinition.toFieldValueException(value); record.setValue(fieldName, value); } catch (final ScriptException e) { Exceptions.throwUncheckedException(e); } } private void validateExpression() { boolean valid = true; final String scriptText = this.expressionField.getText(); if (scriptText.isEmpty()) { valid = false; } else { try { this.script = this.scriptEngineCompiler.compile(scriptText); final Bindings bindings = newBindings(); final AbstractRecordLayer layer = getLayer(); final RecordDefinition recordDefinition = layer.getRecordDefinition(); final Record record = new ArrayRecord(recordDefinition); for (final FieldDefinition field : layer.getFieldDefinitions()) { bindings.put(field.getName(), null); } bindings.put("record", record); this.script.eval(bindings); this.errorsField.setText(null); } catch (final Throwable e) { String errorMessage = e.getMessage(); if (!Property.hasValue(errorMessage)) { errorMessage = "null pointer"; } this.errorsField.setText(errorMessage); this.errorsField.setCaretPosition(0); valid = false; this.script = null; } } setFormValid(valid); } }