/** * * Copyright * 2009-2015 Jayway Products AB * 2016-2017 Föreningen Sambruk * * Licensed under AGPL, Version 3.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.gnu.org/licenses/agpl.txt * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package se.streamsource.streamflow.client.util; import java.awt.Component; import java.awt.Dialog; import java.awt.Frame; import java.awt.KeyboardFocusManager; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Observable; import java.util.ResourceBundle; import java.util.Set; import javax.swing.InputVerifier; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPasswordField; import javax.swing.JScrollPane; import javax.swing.JTextArea; import javax.swing.JTextField; import javax.swing.SwingUtilities; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.text.BadLocationException; import javax.swing.text.JTextComponent; import org.jdesktop.application.ResourceMap; import org.jdesktop.swingx.JXDatePicker; import org.jdesktop.swingx.JXDialog; import org.jdesktop.swingx.util.WindowUtils; import org.qi4j.api.constraint.ConstraintViolationException; import org.qi4j.api.injection.scope.Service; import org.qi4j.api.property.GenericPropertyInfo; import org.qi4j.api.property.Property; import org.qi4j.api.util.DateFunctions; import org.qi4j.library.constraints.annotation.MaxLength; import org.qi4j.runtime.composite.ConstraintsCheck; import org.qi4j.runtime.property.PropertyInstance; import se.streamsource.streamflow.client.StreamflowResources; import se.streamsource.streamflow.client.ui.administration.AdministrationResources; import se.streamsource.streamflow.client.ui.workspace.cases.contacts.StreetAddressSuggestTextField; import se.streamsource.streamflow.client.ui.workspace.cases.general.RemovableLabel; import se.streamsource.streamflow.client.ui.workspace.cases.general.forms.AbstractFieldPanel; import se.streamsource.streamflow.client.ui.workspace.cases.general.forms.AttachmentFieldPanel; import se.streamsource.streamflow.client.ui.workspace.cases.general.forms.CheckboxesPanel; import se.streamsource.streamflow.client.ui.workspace.cases.general.forms.ComboBoxPanel; import se.streamsource.streamflow.client.ui.workspace.cases.general.forms.DatePanel; import se.streamsource.streamflow.client.ui.workspace.cases.general.forms.ListBoxPanel; import se.streamsource.streamflow.client.ui.workspace.cases.general.forms.NumberPanel; import se.streamsource.streamflow.client.ui.workspace.cases.general.forms.OpenSelectionPanel; import se.streamsource.streamflow.client.ui.workspace.cases.general.forms.OptionButtonsPanel; import se.streamsource.streamflow.client.ui.workspace.cases.general.forms.TextAreaFieldPanel; import se.streamsource.streamflow.client.ui.workspace.cases.general.forms.TextFieldPanel; import se.streamsource.streamflow.client.ui.workspace.cases.general.forms.geo.GeoLocationFieldPanel; import se.streamsource.streamflow.client.util.dialog.DialogService; /** * Use ActionBinder+ValueBinder instead */ @Deprecated public class StateBinder extends Observable { @Service DialogService dialogs; ResourceBundle errorMessages; Map<Class<? extends Component>, Binder> binders = new HashMap<Class<? extends Component>, Binder>(); Set<Binding> bindings = new HashSet<Binding>(); Set<Converter> converters = new HashSet<Converter>(); public StateBinder() { Binder defaultBinder = new DefaultBinder( this ); registerBinder( defaultBinder, AbstractFieldPanel.class, TextFieldPanel.class, GeoLocationFieldPanel.class, TextAreaFieldPanel.class, NumberPanel.class, CheckboxesPanel.class, OptionButtonsPanel.class, OpenSelectionPanel.class, ListBoxPanel.class, ComboBoxPanel.class, DatePanel.class, AttachmentFieldPanel.class, JLabel.class, JTextField.class, JTextArea.class, JScrollPane.class, JPasswordField.class, JCheckBox.class, JXDatePicker.class, JComboBox.class, RemovableLabel.class, StreetAddressSuggestTextField.class); errorMessages = ResourceBundle.getBundle( getClass().getName() ); } public void setResourceMap( final ResourceMap resourceMap ) { errorMessages = new ResourceBundle() { protected Object handleGetObject( String key ) { return resourceMap.getString( key ); } public Enumeration<String> getKeys() { return Collections.enumeration( resourceMap.keySet() ); } }; } public void addConverter( Converter converter ) { converters.add( converter ); } public void registerBinder( Binder binder, Class<? extends Component>... componentTypes ) { for (Class<? extends Component> componentType : componentTypes) { binders.put( componentType, binder ); } } public <T extends Component> T bind( T component, final Property property ) { Binder binder = binders.get( component.getClass() ); if (binder == null) throw new IllegalArgumentException( "No binder registered for component type:" + component.getClass().getSimpleName() ); Binding binding; if (property instanceof BinderPropertyInstance) { BinderPropertyInstance binderProperty = (BinderPropertyInstance) property; binding = binder.bind( component, binderProperty.accessor() ); } else { binding = binder.bind( component, property ); } bindings.add( binding ); return component; } public <T> T updateWith( T source ) { for (Binding binding : bindings) { binding.updateWith( source ); } return source; } public void update() { for (Binding binding : bindings) { binding.update(); } } public void handleException( Component component, Exception e ) { component.requestFocus(); if (e instanceof ConstraintViolationException) { ConstraintViolationException cve = (ConstraintViolationException) e; String[] messages = cve.getLocalizedMessages( errorMessages ); StringBuilder message = new StringBuilder( "<html>" ); for (String s : messages) { message.append( s ).append( "<br/>" ); } message.append( "</html>" ); JLabel messageLabel = new JLabel( message.toString() ); JOptionPane.showMessageDialog( component, messageLabel, errorMessages.getString( "property_constraint_violation" ), JOptionPane.ERROR_MESSAGE ); } else { Thread.currentThread().getUncaughtExceptionHandler().uncaughtException( Thread.currentThread(), e ); } } public <T> T bindingTemplate( Class<T> mixinType ) { return (T) Proxy.newProxyInstance( mixinType.getClassLoader(), new Class[]{mixinType}, new InvocationHandler() { public Object invoke( Object proxy, Method method, Object[] args ) throws Throwable { if (Property.class.isAssignableFrom( method.getReturnType() )) { return new BinderPropertyInstance( method, new GenericPropertyInfo( method ), null, null ); } else return null; } } ); } public Component[] boundComponents() { List<Component> components = new ArrayList<Component>(); for (Binding binding : bindings) { components.add( binding.component ); } return components.toArray( new Component[components.size()] ); } private static class BinderPropertyInstance extends PropertyInstance { private Method accessor; public BinderPropertyInstance( Method accessor, GenericPropertyInfo genericPropertyInfo, Object o, ConstraintsCheck o1 ) { super( genericPropertyInfo, o, o1 ); this.accessor = accessor; } public Method accessor() { return accessor; } } interface Binder { Binding bind( Component component, Object property ); void updateComponent( Component component, Object value ); } public class Binding { private Binder binder; private Object source; private Component component; private Object property; private StateBinder stateBinder; Binding( Binder binder, Component component, Object property, StateBinder stateBinder ) { this.binder = binder; this.component = component; this.property = property; this.stateBinder = stateBinder; } void updateWith( Object source ) { this.source = source; update(); } void update() { if (source == null) return; try { Object newValue = property().get(); if (newValue != null) { // Try all converters Object convertedValue; for (Converter converter : converters) { convertedValue = converter.toComponent( newValue ); if (convertedValue != newValue) { newValue = convertedValue; break; } } } binder.updateComponent( component, newValue ); } catch (Exception e) { stateBinder.handleException( component, e ); } } public void updateProperty( Object newValue ) throws IllegalArgumentException { Property<Object> objectProperty = property(); if (objectProperty == null) return; if (newValue != null) { // Try all converters Object convertedValue; for (Converter converter : converters) { convertedValue = converter.fromComponent( newValue ); if (convertedValue != newValue) { newValue = convertedValue; break; } } } if (objectProperty.get() == null && newValue == null) return; if (objectProperty.get() != null && objectProperty.get().equals( newValue )) return; // Do nothing objectProperty.set( newValue ); setChanged(); notifyObservers( objectProperty ); } Property<Object> property() { if (property instanceof Method) { try { return (Property<Object>) ((Method) property).invoke( source ); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } return null; // Should not be possible... } else { return (Property<Object>) property; } } public Object getConstraint( Class annotationClass ) { if (property instanceof Method) { return ((Method) property).getAnnotation( annotationClass ); } else { return ((Property<Object>) property).metaInfo( annotationClass ); } } } private class DefaultBinder implements Binder { private StateBinder stateBinder; public DefaultBinder( StateBinder stateBinder ) { this.stateBinder = stateBinder; } public Binding bind( Component component, Object property ) { final Binding binding = new Binding( this, component, property, stateBinder ); if (component instanceof AbstractFieldPanel) { ((AbstractFieldPanel) component).setBinding( binding ); return binding; } else if (component instanceof JPasswordField) { final JPasswordField passwordField = (JPasswordField) component; passwordField.setInputVerifier( new PropertyInputVerifier( binding ) ); passwordField.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent e ) { KeyboardFocusManager.getCurrentKeyboardFocusManager().focusNextComponent( passwordField ); } } ); return binding; } else if (component instanceof JTextField) { final JTextField textField = (JTextField) component; textField.setInputVerifier( new PropertyInputVerifier( binding ) ); textField.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent e ) { KeyboardFocusManager.getCurrentKeyboardFocusManager().focusNextComponent( textField ); } } ); if (binding.getConstraint( MaxLength.class ) != null) { final MaxLength maxLength = ((MaxLength) binding.getConstraint( MaxLength.class )); textField.getDocument().addDocumentListener( new DocumentListener() { public void insertUpdate( DocumentEvent e ) { if (textField.getDocument().getLength() > maxLength.value()) { dialogs.showMessageDialog( textField, new MessageFormat( i18n.text( StreamflowResources.max_length ) ).format( new Object[]{"" + maxLength.value()} ).toString(), i18n.text( StreamflowResources.invalid_input ) ); SwingUtilities.invokeLater( new Runnable() { public void run() { try { textField.setText( textField.getDocument().getText( 0, maxLength.value() ) ); } catch (BadLocationException e1) { // do nothing } } } ); } } public void removeUpdate( DocumentEvent e ) { } public void changedUpdate( DocumentEvent e ) { } } ); } return binding; } else if (component instanceof JTextArea) { final JTextArea textArea = (JTextArea) component; textArea.setInputVerifier( new PropertyInputVerifier( binding ) ); return binding; } else if (component instanceof JScrollPane) { JScrollPane pane = (JScrollPane) component; return bind( pane.getViewport().getView(), property ); } else if (component instanceof JCheckBox) { final JCheckBox checkBox = (JCheckBox) component; checkBox.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent e ) { binding.updateProperty( checkBox.isSelected() ); } } ); return binding; } else if (component instanceof JLabel) { // Do nothing return binding; } else if (component instanceof JXDatePicker) { final JXDatePicker datePicker = (JXDatePicker) component; datePicker.setInputVerifier( new PropertyInputVerifier( binding ) ); datePicker.addPropertyChangeListener( new PropertyChangeListener() { public void propertyChange( PropertyChangeEvent e ) { if ("date".equals( e.getPropertyName() )) { binding.updateProperty( e.getNewValue() ); } } } ); return binding; } else if (component instanceof JComboBox) { final JComboBox comboBox = (JComboBox) component; comboBox.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent actionEvent ) { binding.updateProperty( ((JComboBox) actionEvent.getSource()).getSelectedItem() ); } } ); return binding; } else if (component instanceof RemovableLabel) { final RemovableLabel removableLabel = (RemovableLabel) component; removableLabel.getButton().addActionListener( new ActionListener() { public void actionPerformed(ActionEvent e) { // removableLabel.setListItemValue( null ); binding.updateProperty( null ); } } ); return binding; } else if (component instanceof StreetAddressSuggestTextField ) { final StreetAddressSuggestTextField suggestTextfield = (StreetAddressSuggestTextField) component; suggestTextfield.setBinding( binding); return binding; } throw new IllegalArgumentException( "Could not bind to component of type " + component.getClass().getName() ); } public void updateComponent( Component component, Object value ) { if (component instanceof AbstractFieldPanel) { AbstractFieldPanel panel = (AbstractFieldPanel) component; panel.setValue( value == null ? "" : value.toString() ); } else if (component instanceof JPasswordField) { JPasswordField passwordField = (JPasswordField) component; passwordField.setText( value == null ? "" : value.toString() ); } else if (component instanceof JTextComponent) { JTextComponent textField = (JTextComponent) component; String text = value == null ? "" : value.toString(); textField.setText( text ); textField.setCaretPosition( 0 ); } else if (component instanceof JCheckBox) { JCheckBox checkBox = (JCheckBox) component; checkBox.setSelected( ((Boolean) value) ); } else if (component instanceof JLabel) { JLabel label = (JLabel) component; label.setText( value == null ? "" : value.toString() ); } else if (component instanceof JXDatePicker) { JXDatePicker datePicker = (JXDatePicker) component; if (value instanceof String) { if (!((String) value).isEmpty()) { datePicker.setDate( DateFunctions.fromString( (String) value ) ); } } else { datePicker.setDate( (Date) value ); } } else if (component instanceof JComboBox) { JComboBox box = (JComboBox) component; box.setSelectedItem( value ); } else if (component instanceof RemovableLabel) { RemovableLabel removableLabel = (RemovableLabel) component; removableLabel.setText( (String) value ); } else if (component instanceof StreetAddressSuggestTextField) { StreetAddressSuggestTextField suggestField = (StreetAddressSuggestTextField) component; suggestField.getTextField().setText( value == null ? "" : value.toString() ); } } } class PropertyInputVerifier extends InputVerifier { private Binding binding; PropertyInputVerifier( Binding binding ) { this.binding = binding; } IllegalArgumentException exception; public boolean verify( JComponent input ) { try { Object value = null; if (input instanceof JTextComponent) { value = ((JTextComponent) input).getText(); } else if (input instanceof JXDatePicker) { value = ((JXDatePicker) input).getDate(); } binding.updateProperty( value ); return true; } catch (IllegalArgumentException e) { exception = e; return false; } } @Override public boolean shouldYieldFocus( JComponent input ) { boolean result = super.shouldYieldFocus( input ); if (!result) { Window window = WindowUtils.findWindow( input ); StringBuilder message = new StringBuilder( i18n.text( AdministrationResources.invalid_value ) ); if (exception instanceof ConstraintViolationException) { ConstraintViolationException ex = (ConstraintViolationException) exception; String[] messages = ex.getLocalizedMessages( errorMessages ); message = new StringBuilder( "<html>" ); for (String s : messages) { message.append( "<p>" ).append( s ).append( "</p>" ); } message.append( "</html>" ); } JLabel main = new JLabel( message.toString() ); JXDialog dialog; if (window instanceof Frame) dialog = new JXDialog( (Frame) window, main ); else dialog = new JXDialog( (Dialog) window, main ); dialog.setModal( true ); dialog.pack(); dialog.setLocationRelativeTo( SwingUtilities.windowForComponent( input ) ); dialog.setVisible( true ); } return result; } } public interface Converter { Object toComponent( Object value ); Object fromComponent( Object value ); } }