Why is firePropertyChange(String propertyName, Object oldValue, Object newValue) protected and not public?

13.8k views Asked by At

Well the thing is I'm working on a IDateEditor interface implementation from JCalendar library and I've noticed that Component.firePropertyChange(String propertyName, Object oldValue, Object newValue) method is not public but protected. The situation is illustrated below:

public class DateFormattedTextField implements IDateEditor {

    private JFormattedTextField editor;        
    private DateUtil dateUtil;

    ...

    @Override
    public void setDate(Date date) {
        Date oldDate = (Date)editor.getValue();            
        if(dateUtil.checkDate(date)){
            editor.setValue(date);
            editor.firePropertyChange("date", oldDate, date); // <-- error here
        }
    }

}

As you can see I'm not able to fire a property change because of this method being protected. Of course if I make my class extending from JFormattedTextfield instead of using a simple variable I could easily get rid of this problem.

public class DateFormattedTextField extends JFormattedTextField implements IDateEditor {

    private DateUtil dateUtil;

    ...

    @Override
    public void setDate(Date date) {
        Date oldDate = (Date)getValue();            
        if(dateUtil.checkDate(date)){
            setValue(date);
            firePropertyChange("date", oldDate, date); // <-- No problem here
        }
    }
}

But that's not what I'm asking. I would like to know: why is this method protected?

I know it should be some design matter but I can't figure out why is that, especially considering that most of methods to fire property change events are public:

enter image description here

Maybe most experienced developers can shed some light on this. Thanks in advance.

Addendum:

Here is my code so far. Feel free to use/modify/play-with it.

public class DateFormattedTextField implements IDateEditor {

    private JFormattedTextField editor;        
    private DateUtil dateUtil;
    private DateFormat dateFormat;
    private String dateFormatString;

    public DateFormattedTextField(){
        dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM);
        editor = new JFormattedTextField(dateFormat);
        editor.setColumns(10);
        editor.setFocusLostBehavior(JFormattedTextField.COMMIT_OR_REVERT);
        dateUtil = new DateUtil();            
    }

    @Override
    public Date getDate() {
        return (Date)editor.getValue();
    }

    @Override
    public void setDate(Date date) {
        Date oldDate = (Date)editor.getValue();            
        if(dateUtil.checkDate(date)){
            editor.setValue(date);
            editor.firePropertyChange("date", oldDate, date); // <-- error here
        }
    }

    @Override
    public void setDateFormatString(String dateFormatString) {
        this.dateFormatString = dateFormatString;
    }

    @Override
    public String getDateFormatString() {
        return this.dateFormatString;
    }

    @Override
    public void setSelectableDateRange(Date min, Date max) {
        dateUtil.setSelectableDateRange(min, max);
    }

    @Override
    public Date getMaxSelectableDate() {
        return dateUtil.getMaxSelectableDate();
    }

    @Override
    public Date getMinSelectableDate() {
        return dateUtil.getMinSelectableDate();
    }

    @Override
    public void setMaxSelectableDate(Date max) {
        dateUtil.setMaxSelectableDate(max);
    }

    @Override
    public void setMinSelectableDate(Date min) {
        dateUtil.setMinSelectableDate(min);
    }

    @Override
    public JComponent getUiComponent() {
        return editor;
    }

    @Override
    public void setLocale(Locale locale) {
        editor.setLocale(locale); // to be reviewed
    }

    @Override
    public void setEnabled(boolean enabled) {
        editor.setEnabled(enabled);
    }

    @Override
    public void addPropertyChangeListener(PropertyChangeListener listener) {
        editor.addPropertyChangeListener(listener);
    }

    @Override
    public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
        editor.addPropertyChangeListener(propertyName, listener);
    }

    @Override
    public void removePropertyChangeListener(PropertyChangeListener listener) {
        editor.removePropertyChangeListener(listener);
    }

    @Override
    public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
        editor.removePropertyChangeListener(propertyName, listener);
    }
}
2

There are 2 answers

2
kleopatra On BEST ANSWER

Change notification is the inherent responsibility of any observable (in general and a java bean in particular): it would violate its contract if it wouldn't fire a PropertyChangeEvent when any of its bound properties is changed. That being the case, no other party ever needs to use the fireXX methods.

So it simply doesn't make any sense to have a scope wider than protected. If you feel like needing it, you are doing something wrong.

0
dic19 On

I'm adding this answer just for completeness sake. Following @kleopatra's wise explanation and comments, I've realized that I was mixing concepts and responsibilities. In this case property change notification is not a responsibility of the JFormattedTextField used as underlying editor component but IDateEditor implementation itself.

So, in order to fulfill the interface I've used PropertyChangeSupport to keep a list of property change listeners and notify them on a PropertyChangeEvent:

public class DefaultDateEditor implements IDateEditor {
    ...
    private final JFormattedTextField editor;
    private final PropertyChangeSupport propertyChangeSupport;
    ...

    public DefaultDateEditor() {
        ...
        propertyChangeSupport = new PropertyChangeSupport(this);
        ...
        editor = new JFormattedTextField();
        ...
    }

    @Override
    public final void addPropertyChangeListener(PropertyChangeListener listener) {
        propertyChangeSupport.addPropertyChangeListener(listener);
    }

    @Override
    public final void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
        propertyChangeSupport.addPropertyChangeListener(propertyName, listener);
    }

    @Override
    public final void removePropertyChangeListener(PropertyChangeListener listener) {
        propertyChangeSupport.removePropertyChangeListener(listener);
    }

    @Override
    public final void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
        propertyChangeSupport.removePropertyChangeListener(propertyName, listener);
    }

    public final PropertyChangeListener[] getPropertyChangeListeners() {
        return propertyChangeSupport.getPropertyChangeListeners();
    }

    public final PropertyChangeListener[] getPropertyChangeListeners(String propertyName) {
        return propertyChangeSupport.getPropertyChangeListeners(propertyName);
    }

    protected final void firePropertyChangeEvent(String propertyName, Object oldValue, Object newValue) {
        propertyChangeSupport.firePropertyChange(propertyName, oldValue, newValue);
    }
    ...
}

As you can see the task to add/remove PropertyChangeListeners and notify them on property change events is delegated to the PropertyChangeSupport class member, but the source of the event is this, that is the IDateEditor interface implementer. As @kleopatra said that is the source that clients would expect to be the source of the event, not the underlyining editor component.

Below is the complete code of the resulting class (sorry for the extension). Please feel free of use / modify or play-with it if you like.

/**
 * Custom implementation of {@code IDateEditor} interface. Unlike the default 
 * implementation provided with JCalendar library, this won't allow invalid 
 * inputs of any kind from the user.
 * 
 * @author dic19
 */
public class DefaultDateEditor implements IDateEditor {

    private Date date;
    private String dateFormatString;
    private Locale locale;
    private SimpleDateFormat dateFormat;
    private final DateFormatter dateFormatter;
    private final JFormattedTextField editor;
    private final DateUtil dateUtil;
    private final PropertyChangeSupport propertyChangeSupport;

    public DefaultDateEditor() {
        date = new Date();
        dateUtil = new DateUtil();
        propertyChangeSupport = new PropertyChangeSupport(this);
        addPropertyChangeListener("dateFormatString", new DateFormatStringChangeListener());
        addPropertyChangeListener("locale", new LocaleChangeListener());

        dateFormatString = "yyyy-MM-dd HH:mm:ss";
        locale = Locale.getDefault();
        dateFormat = (SimpleDateFormat)DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, locale);
        dateFormat.applyPattern(dateFormatString);

        dateFormatter = new DateFormatter();
        dateFormatter.setCommitsOnValidEdit(true);
        dateFormatter.setAllowsInvalid(false);
        dateFormatter.setOverwriteMode(true);

        editor = new JFormattedTextField();
        editor.setValue(date);
        editor.setColumns(15);
        editor.setToolTipText(dateFormatString);
        editor.addPropertyChangeListener("value", new EditorValueChangeListener());

        installFormatterFactoryOnEditor();
    }

    private void installFormatterFactoryOnEditor() {
        dateFormatter.setFormat(dateFormat);
        DefaultFormatterFactory factory = editor.getFormatterFactory() instanceof DefaultFormatterFactory
                                        ? (DefaultFormatterFactory) editor.getFormatterFactory()
                                        : new DefaultFormatterFactory();
        factory.setDefaultFormatter(dateFormatter);
        factory.setDisplayFormatter(dateFormatter);
        factory.setEditFormatter(dateFormatter);
        factory.setNullFormatter(dateFormatter);
        editor.setFormatterFactory(factory);
    }

    @Override
    public Date getDate() {
        return date != null ? new Date(date.getTime()) : null;
    }

    @Override
    public void setDate(Date date) {
        if (dateUtil.checkDate(date)) {
            Date oldValue = this.date;
            this.date = date != null ? new Date(date.getTime()) : null;
            editor.setValue(this.date);
            firePropertyChangeEvent("date", oldValue, date);
        }
    }

    @Override
    public void setDateFormatString(String dateFormatString) {
        String oldDateFormat = this.dateFormatString;
        this.dateFormatString = dateFormatString;
        firePropertyChangeEvent("dateFormatString", oldDateFormat, dateFormatString);
    }

    @Override
    public String getDateFormatString() {
        return dateFormatString;
    }

    @Override
    public void setSelectableDateRange(Date min, Date max) {
        dateUtil.setSelectableDateRange(min, max);
    }

    @Override
    public Date getMaxSelectableDate() {
        return dateUtil.getMaxSelectableDate();
    }

    @Override
    public Date getMinSelectableDate() {
        return dateUtil.getMinSelectableDate();
    }

    @Override
    public void setMaxSelectableDate(Date max) {
        dateUtil.setMaxSelectableDate(max);
    }

    @Override
    public void setMinSelectableDate(Date min) {
        dateUtil.setMinSelectableDate(min);
    }

    @Override
    public JComponent getUiComponent() {
        return editor;
    }

    @Override
    public void setLocale(Locale locale) {
        Locale oldLocale = this.locale;
        this.locale = locale;
        firePropertyChangeEvent("locale", oldLocale, locale);
    }

    @Override
    public void setEnabled(boolean enabled) {
        editor.setEnabled(enabled);
    }

    @Override
    public final void addPropertyChangeListener(PropertyChangeListener listener) {
        propertyChangeSupport.addPropertyChangeListener(listener);
    }

    @Override
    public final void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
        propertyChangeSupport.addPropertyChangeListener(propertyName, listener);
    }

    @Override
    public final void removePropertyChangeListener(PropertyChangeListener listener) {
        propertyChangeSupport.removePropertyChangeListener(listener);
    }

    @Override
    public final void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
        propertyChangeSupport.removePropertyChangeListener(propertyName, listener);
    }

    public final PropertyChangeListener[] getPropertyChangeListeners() {
        return propertyChangeSupport.getPropertyChangeListeners();
    }

    public final PropertyChangeListener[] getPropertyChangeListeners(String propertyName) {
        return propertyChangeSupport.getPropertyChangeListeners(propertyName);
    }

    protected final void firePropertyChangeEvent(String propertyName, Object oldValue, Object newValue) {
        propertyChangeSupport.firePropertyChange(propertyName, oldValue, newValue);
    }

    private class EditorValueChangeListener implements PropertyChangeListener {
        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            if ("value".equals(evt.getPropertyName())) {
                System.out.println("Old value:" + evt.getOldValue());
                System.out.println("New value:" + evt.getNewValue());
                setDate((Date)evt.getNewValue());
            }
        }
    }

    private class DateFormatStringChangeListener implements PropertyChangeListener {
        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            if ("dateFormatString".equals(evt.getPropertyName())) {
                dateFormat.applyPattern(dateFormatString);
                editor.setToolTipText(dateFormatString);
                installFormatterFactoryOnEditor();                    
            }
        }
    }

    private class LocaleChangeListener implements PropertyChangeListener {
        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            if ("locale".equals(evt.getPropertyName())) {
                dateFormat = (SimpleDateFormat)DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, locale);
                dateFormat.applyPattern(dateFormatString);
                editor.setLocale(locale);
                installFormatterFactoryOnEditor();
            }
        }
    }
}