alvinalexander.com | career | drupal | java | mac | mysql | perl | scala | uml | unix  

What this is

This file is included in the DevDaily.com "Java Source Code Warehouse" project. The intent of this project is to help you "Learn Java by Example" TM.

Other links

The source code

/*
 *                 Sun Public License Notice
 * 
 * The contents of this file are subject to the Sun Public License
 * Version 1.0 (the "License"). You may not use this file except in
 * compliance with the License. A copy of the License is available at
 * http://www.sun.com/
 * 
 * The Original Code is NetBeans. The Initial Developer of the Original
 * Code is Sun Microsystems, Inc. Portions Copyright 1997-2004 Sun
 * Microsystems, Inc. All Rights Reserved.
 */

package org.netbeans.modules.java;

import java.io.*;
import java.awt.Component;
import java.awt.Dialog;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.text.MessageFormat;
import java.util.*;

import javax.swing.*;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.border.*;

import org.openide.cookies.ConnectionCookie;
import org.openide.nodes.Node;
import org.openide.src.*;
import org.openide.util.RequestProcessor;
import org.openide.util.HelpCtx;
import org.netbeans.modules.java.settings.JavaSynchronizationSettings;

import org.netbeans.api.java.comparators.JavaElementComparator;
import org.netbeans.modules.javacore.internalapi.JavaMetamodel;

import org.openide.DialogDescriptor;
import org.openide.DialogDisplayer;
import org.openide.ErrorManager;
import org.openide.util.NbBundle;

/** This class and its inner classes maintain the basic functionality and support
* for the synchronization between java sources.
*
* @author Petr Hamernik
*/
public class JavaConnections {
    /** Constants used for definitions of the TYPE_XXX constants */
    public static final int ADD = 0x0001;
    public static final int REMOVE = 0x0002;
    public static final int CHANGE = 0x0004;

    public static final int TYPE_FIELDS_ADD = ADD;
    public static final int TYPE_FIELDS_REMOVE = REMOVE;
    public static final int TYPE_FIELDS_CHANGE = CHANGE;
    public static final int TYPE_FIELDS = (ADD | REMOVE | CHANGE);

    public static final int TYPE_METHODS_ADD = ADD << 4;
    public static final int TYPE_METHODS_REMOVE = REMOVE << 4;
    public static final int TYPE_METHODS_CHANGE = CHANGE << 4;
    public static final int TYPE_METHODS = (ADD | REMOVE | CHANGE) << 4;

    public static final int TYPE_CLASSES_ADD = ADD << 8;
    public static final int TYPE_CLASSES_REMOVE = REMOVE << 8;
    public static final int TYPE_CLASSES_CHANGE = CHANGE << 8;
    public static final int TYPE_CLASSES = (ADD | REMOVE | CHANGE) << 8;

    public static final int TYPE_CONSTRUCTORS_ADD = ADD << 12;
    public static final int TYPE_CONSTRUCTORS_REMOVE = REMOVE << 12;
    public static final int TYPE_CONSTRUCTORS_CHANGE = CHANGE << 12;
    public static final int TYPE_CONSTRUCTORS = (ADD | REMOVE | CHANGE) << 12;

    public static final int TYPE_INITIALIZERS_ADD = ADD << 16;
    public static final int TYPE_INITIALIZERS_REMOVE = REMOVE << 16;
    public static final int TYPE_INITIALIZERS_CHANGE = CHANGE << 16;
    public static final int TYPE_INITIALIZERS = (ADD | REMOVE | CHANGE) << 16;

    /**
     * @deprecated this flag served as a tag that the subsystem should check its own
     * integrity. Should not be used.
     */
    public static final int TYPE_SOURCE_CHECK_SELF = 0x100000;
    
    /**
     * @deprecated this flag was used as a hack - it forced deep synchronization of the
     * data object. Should not be used any more.
     */
    public static final int TYPE_SOURCE_CHECK_DEEP = 0x200000;

    public static final int ADD_MASK = 0x11111;
    public static final int REMOVE_MASK = 0x22222;
    public static final int CHANGE_MASK = 0x44444;

    public static final int TYPE_ALL = 0xFFFFF;

    static final JavaSynchronizationSettings SETTINGS =
        (JavaSynchronizationSettings)JavaSynchronizationSettings.findObject(JavaSynchronizationSettings.class);

    /** The basic type of connection between two sources.
    */
    public static class Type implements ConnectionCookie.Type {

        /** The filter of events */
        int filter;

        static final long serialVersionUID =-6323669534600303244L;
        /** Creates new type of java connection.
        * @param filter The filter - consist of TYPE_XXX constants.
        */
        public Type(int filter) {
            this.filter = filter;
        }

        /** The class that is passed into the listener's notify
        * method when an event of this type is fired.
        *
        * @return ImplementsEvent.class
        */
        public Class getEventClass () {
            return Event.class;
        }

        /** Implements connection is persistent.
        * @return always true
        */
        public boolean isPersistent () {
            return true;
        }

        /** Test whether this type overlaps with the specified one.
        * @return true if the types are overlapped
        */
        public boolean overlaps(ConnectionCookie.Type type) {
            if (type instanceof Type)
                return (filter & ((Type)type).filter) != 0;
            return false;
        }

        /**
        * @return the filter of this type which was passed in the constructor
        */
        public int getFilter() {
            return filter;
        }

        public boolean equals(Object o) {
            return (o instanceof Type) && (((Type)o).filter == filter);
        }

        public int hashCode() {
            return filter;
        }
    };

    /** The basic type of connection between two sources
    * - interface and implementation.
    */
    public static final Type IMPLEMENTS = new Type(TYPE_METHODS_ADD | TYPE_METHODS_CHANGE);

    //================================================

    public static class Change {
        int changeType;

        Element oldElement;
        Element newElement;
        Element[] elements;

        Change(int changeType) {
            this(changeType, null, null, null);
        }

        Change(int changeType, Element[] elements) {
            this(changeType, null, null, elements);
        }

        Change(int changeType, Element oldElement, Element newElement) {
            this(changeType, oldElement, newElement, null);
        }

        protected Change(int changeType, Element oldElement, Element newElement, Element[] elements) {
            this.changeType = changeType;
            this.oldElement = oldElement;
            this.newElement = newElement;
            this.elements = elements;
        }

        public int getChangeType() {
            return changeType;
        }

        public Element getNewElement() {
            return newElement;
        }

        public Element getOldElement() {
            return oldElement;
        }

        public Element[] getElements() {
            return elements;
        }
    }

    public static class Event extends ConnectionCookie.Event {
        Change[] changes;

        static final long serialVersionUID =-3347417315663192416L;

        public Event(Node n, Change[] changes, Type type) {
            super(n, type);
            this.changes = changes;
        }

        public Change[] getChanges() {
            return changes;
        }
    }

    /** This class represents one change during connection
    * synchronization between two classes. If user accept it,
    * there could be called process method to 
    * make the change.
    */
    public abstract static class ChangeProcessor {
        /** Display name of the change */
        private String displayName;
        
        /**
         * Holds element that serves as a model.
         */
        private Element model;

        /** Create new change
        * @param displayName Display name of the change
        */
        protected ChangeProcessor(String displayName, Element model) {
            this.displayName = displayName;
            this.model = model;
        }
        
        protected ChangeProcessor(String displayName) {
            this(displayName, null);
        }
        
        /**
        * @return Display name of the change
        */
        public String getDisplayName() {
            return displayName;
        }
        
        public Element getModel() {
            return model;
        }
        
        /** Process the change.
        * @exception SourceException if any problem occured with
        *     modification of the source code.
        */
        public abstract void process() throws SourceException;
    }

    static RequestProcessor SYNCHRONIZATION_RP = new RequestProcessor("Synchronization RP"); // NOI18N
    
    /** Encapsulation of user-confirmation request.
     *  The class holds a task that can be waited for and the result of user interaction
     *  with the Confirmation dialog.
     */
    public static class SyncRequest implements Runnable {
        private byte    syncType;
        private List    changes;
        private RequestProcessor.Task task;
        private String  caption;
        
        SyncRequest(String caption, List changes, byte type) {
            this.changes = changes;
            this.caption = caption;
            syncType = type;
            task = SYNCHRONIZATION_RP.post(this);
        }

        /** Returns a task, that can be used for waiting for the end of the user
         * interaction (closing the dialog), or cancelling the task (discarding
         * the sync request
         */
        public RequestProcessor.Task getTask() {
            return task;
        }
        
        public String getCaption() {
            return caption;
        }
        
        /** Retrieves the synchronization type the user has selected in the
         * Confirm Changes dialog.
         * @return value of the sync option user has selected.
         */
        public byte getSyncType() {
            return syncType;
        }
        
        /** Helper function that blocks the caller until the user closes the
         * dialog box. Do not call this function from AWT thread!
         */
        public void waitFinished() {
            getTask().waitFinished();
        }

        public synchronized void run() {
	    try {
        	SwingUtilities.invokeAndWait(new Runnable() {
		    public void run() {
			syncType = reallyShowChangesDialog(caption, changes, syncType);
		    }
		});
	    } catch (InterruptedException ex) {
	    } catch (java.lang.reflect.InvocationTargetException ex) {
	    }
        }
        
    }
    
    /** Schedules opening of dialog box that displays changes in the list and asks
     * the user for confirmation. The list must contain subtypes of JavaConnection.ChangeProcessor.
     * The dialog might not be displayed immediately, the action is only scheduled to a
     * private RequestProcessor.
     * This method supersedes {@link #showChangesDialog} method in that it allows more freedom
     * in multithread environment.
     * @param changes list of changes that should be presented to the user. All items must be
     *                subclasses of JavaConnections.ChangeProcessor.
     * @param synchType value of synchronization settings that is initially selected in
     *                the dialog.
     * @return request object that can be used for waiting for the completion of the request,
     *  or querying for the new synchronization setting.
     */
    public static SyncRequest scheduleShowChangesDialog(String caption, List changes, byte synchType) {
        return new SyncRequest(caption, changes, synchType);
    }
    
    public static SyncRequest scheduleShowChangesDialog(List changes, byte synchType) {
        return scheduleShowChangesDialog(null, changes, synchType);
    }
    
    
   /** Opens the dialog with found changes and ask user
    * to confirm them. Note: calling the method from the AWT thread might display
    * another instance of the dialog, if the confirmation dialog is already opened for another
    * request. There's no simple way how to block the caller while processing requests for
    * the old dialog. If possible, call scheduleShowChangesDialog() and attach a TaskListener
    * for the created task.
    * @param changes The list of the ChangeProcessor objects
    * @param synchType the current synchronization type
    * @return new value of synchronizationType after dialog closed.
    */
    public static byte showChangesDialog(String caption, List changes, byte synchType) {
        if (SwingUtilities.isEventDispatchThread()) {
            return reallyShowChangesDialog(caption, changes, synchType);
        } else {
            SyncRequest rq = scheduleShowChangesDialog(caption, changes, synchType);
            rq.waitFinished();
            return rq.getSyncType();
        }
    }
    
    public static byte showChangesDialog(List changes, byte synchType) {
        return showChangesDialog(null, changes, synchType);
    }
    
    /** Opens the dialog with found changes and ask user
    * to confirm them.
    * @param changes The list of the ChangeProcessor objects
    * @param synchType the current synchronization type
    * @return new value of synchronizationType after dialog closed.
    */
    private static byte reallyShowChangesDialog(String caption, List changes, byte synchType) {
        final JButton processButton;
        final JButton processAllButton;
        final JButton closeButton;
        final ConnectionPanel connectionPanel;
        final DialogDescriptor confirmChangesDescriptor;
        final Dialog confirmChangesDialog[] = { null };
        
        processButton = new JButton (Util.getString("LAB_processButton"));
        processButton.setMnemonic(Util.getString("LAB_processButton_Mnemonic").charAt(0));
        processButton.setToolTipText(Util.getString("ACS_processButtonA11yDesc"));
        processAllButton = new JButton (Util.getString("LAB_processAllButton"));
        processAllButton.setMnemonic(Util.getString("LAB_processAllButton_Mnemonic").charAt(0));
        processAllButton.setToolTipText(Util.getString("ACS_processAllButtonA11yDesc"));
        closeButton = new JButton (Util.getString("LAB_closeButton"));
        closeButton.setMnemonic(Util.getString("LAB_closeButton_Mnemonic").charAt(0));
        closeButton.setToolTipText(Util.getString("ACS_closeButtonA11yDesc"));
        final Object [] options = new Object [] {
                                      processButton,
                                      processAllButton
                                  };
        final Object [] additionalOptions = new Object [] {
                                                closeButton
                                            };
        connectionPanel = new ConnectionPanel(caption, processButton);
        confirmChangesDescriptor = new DialogDescriptor(
                                       connectionPanel,
                                       Util.getString("LAB_ConfirmDialog"),
                                       true,
                                       options,
                                       processButton,
                                       DialogDescriptor.RIGHT_ALIGN,
                                       new HelpCtx ("java.sync.csh.confirmchanges"), // NOI18N
                                       new ActionListener() {
                                           public void actionPerformed(ActionEvent e) {
                                               if (e.getSource() instanceof Component) {
                                                   Component root;

                                                   // hack to avoid multiple calls for disposed dialogs:
                                                   root = SwingUtilities.getRoot((Component)e.getSource());
                                                   if (root == null || !root.isDisplayable()) {
                                                       return;
                                                   }
                                               }
                                               if (options[0].equals(e.getSource())) {
                                                   int min = connectionPanel.changesList.getMinSelectionIndex();
                                                   int max = connectionPanel.changesList.getMaxSelectionIndex();
                                                   for (int i = max; i >= min; i--) {
                                                       if (connectionPanel.changesList.isSelectedIndex(i)) {
                                                           final ChangeProcessor p = (ChangeProcessor)connectionPanel.listModel.getElementAt(i);
                                                           try {
                                                               p.process();
                                                           }
                                                           catch (SourceException ee) {
                                                               ErrorManager.getDefault().notify(ee);
                                                               continue;
                                                           }
                                                           connectionPanel.listModel.removeElementAt(i);
                                                       }
                                                   }
                                                   if (connectionPanel.listModel.isEmpty()) {
                                                       confirmChangesDialog[0].setVisible(false);
                                                   }
                                               }
                                               else if (options[1].equals(e.getSource())) {
                                                   // all the changes will run in transaction the improve
                                                   // performance and allow rollback of all changes.
                                                   JavaMetamodel.getDefaultRepository().beginTrans(true);
                                                   JavaMetamodel.getUndoManager().setUndoDescription(
                                                       NbBundle.getMessage(JavaConnections.class, "LAB_SynchronizeAction")
                                                   );
                                                   boolean fail = true;
                                                   try {
                                                   Enumeration en = connectionPanel.listModel.elements();
                                                   while (en.hasMoreElements()) {
                                                       final ChangeProcessor p = (ChangeProcessor) en.nextElement();
                                                       p.process();
                                                       }
                                                       fail = false;
                                                   } catch (SourceException ee) {
                                                           ErrorManager.getDefault().notify(ee);
                                                   } finally {
                                                       JavaMetamodel.getDefaultRepository().endTrans(fail);
                                                   }
                                                   confirmChangesDialog[0].setVisible(false);
                                                   connectionPanel.setChanges(null);
                                               }
                                               else if (additionalOptions[0].equals(e.getSource())) {
                                                   confirmChangesDialog[0].setVisible(false);
                                                   connectionPanel.setChanges(null);
                                               }
                                           }
                                       }
                                   );
        confirmChangesDescriptor.setAdditionalOptions (additionalOptions);
        confirmChangesDescriptor.setClosingOptions(additionalOptions);

        processButton.setEnabled(false);
        processAllButton.requestFocus();
        connectionPanel.setChanges(changes);
        connectionPanel.setRadio(synchType);

        try {
            confirmChangesDialog[0] = DialogDisplayer.getDefault().createDialog(confirmChangesDescriptor);
            confirmChangesDialog[0].show();
            return connectionPanel.getRadio();
        } finally {
            confirmChangesDialog[0].dispose();
        }
    }
    
    private static final Comparator[] LEGACY_COMPARATORS = {
        JavaElementComparator.createMethodComparator(false,
            new int[] { JavaElementComparator.NAME, JavaElementComparator.PARAMETERS }),
        JavaElementComparator.createMethodComparator(false,
            new int[] { JavaElementComparator.PARAMETERS }),
        JavaElementComparator.createMethodComparator(false,
            new int[] { JavaElementComparator.NAME })
    };

    public static void compareMethods(final ClassElement dest,
                                      ClassElement src, List changeProcessors,
                                      String addMessage, String updateMessage) {
        final MethodElement[] oldMethods = dest.getMethods();
        final MethodElement[] newMethods = src.getMethods();
        int newSize = newMethods.length;

        int[] result = InterfaceConnection.pairElements(oldMethods, newMethods, LEGACY_COMPARATORS);

        MessageFormat addMsg = new MessageFormat(addMessage);
        MessageFormat updateMsg = new MessageFormat(updateMessage);
        //final SourceElementImpl source = (SourceElementImpl)dest.getSource().getCookie(SourceElementImpl.class);
        for (int i = 0; i < newSize; i++) {
            final MethodElement m = newMethods[i];
            if (result[i] == -1) {
                changeProcessors.add(new ChangeProcessor(addMsg.format(new Object[] { m.getName().toString() }), m) {
                    public void process() throws SourceException {
                        dest.addMethod(m);
                    }
                });
            } else {
                final MethodElement oldMethod = oldMethods[result[i]];
                MethodParameter[] pars = oldMethod.getParameters();
                Identifier[] excs = oldMethod.getExceptions();
                final boolean[] changes = new boolean[] {
                    !oldMethod.getReturn().compareTo(m.getReturn(), false),
                    pars.length != m.getParameters().length,
                    excs.length != m.getExceptions().length,
                    oldMethod.getModifiers() != m.getModifiers(),
                    !oldMethod.getName().compareTo(m.getName(), false)
                };
                for (int j = 0; (j < pars.length) && !changes[1]; j++)
                    if (!pars[j].compareTo(m.getParameters()[j], false, false))
                        changes[1] = true;
                for (int j = 0; (j < excs.length) && !changes[2]; j++)
                    if (!excs[j].compareTo(m.getExceptions()[j], false))
                        changes[2] = true;
                
                boolean ch = false;
                for (int j = 0; (j < changes.length) && !ch; j++)
                    ch |= changes[j];
                
                if (ch) {
                    changeProcessors.add(new ChangeProcessor(updateMsg.format(new Object[] { oldMethod.getName().toString() }), m) {
                        public void process() throws SourceException {
                            if (changes[0])
                                oldMethod.setReturn(m.getReturn());
                            if (changes[1])
                                oldMethod.setParameters(m.getParameters());
                            if (changes[2])
                                oldMethod.setExceptions(m.getExceptions());
                            if (changes[3])
                                oldMethod.setModifiers(m.getModifiers());
                            if (changes[4])
                                oldMethod.setName(m.getName());
                        }
                    });
                }
            }
        }
    }

    static class ConnectionPanel extends JPanel {
        private JPanel changesPanel;
        private JScrollPane jScrollPane1;
        private JList changesList;
        private JPanel bottomPanel;
        private JPanel radioPanel;
        private JRadioButton radioDisable;
        private JRadioButton radioConfirm;
        private JRadioButton radioAuto;

        DefaultListModel listModel;

        static final long serialVersionUID =-799308237208355590L;

        /** Initializes the Form */
        public ConnectionPanel(String caption, final JButton processButton) {
            setLayout (new java.awt.BorderLayout (0, 12));
            setBorder(new EmptyBorder(12, 12, 11, 0));
            getAccessibleContext().setAccessibleName(Util.getString("ACS_SychronizePanelA11yName"));  // NOI18N
            getAccessibleContext().setAccessibleDescription(Util.getString("ACS_SychronizePanelA11yDesc"));  // NOI18N

            JTextArea text = new JTextArea();
            text.setEnabled(false);
            text.setEditable(false);
            text.setDisabledTextColor(UIManager.getColor("Label.foreground")); // NOI18N
            text.setBackground(UIManager.getColor("Label.background")); // NOI18N
            text.setLineWrap(true);
            text.setWrapStyleWord(true);
            text.setText(caption);
            add(text, "North"); // NOI18N
            
            changesPanel = new JPanel ();
            changesPanel.setLayout (new java.awt.BorderLayout (5, 5));

            JLabel changesLabel = new JLabel();
            changesLabel.setText(Util.getString("LAB_ChangesList"));
            changesLabel.getAccessibleContext().setAccessibleDescription(Util.getString("ACS_ChangesListA11yDesc"));  // NOI18N
            changesPanel.add(changesLabel, "North"); // NOI18N
            
            jScrollPane1 = new JScrollPane ();

            listModel = new DefaultListModel();

            changesList = new JList (listModel);
            changesList.setToolTipText (Util.getString("HINT_ChangesList"));
            changesList.setCellRenderer(new ChangesListCellRenderer());
            changesList.addListSelectionListener(new ListSelectionListener() {
                                                     public void valueChanged(ListSelectionEvent e) {
                                                         processButton.setEnabled(!changesList.isSelectionEmpty());
                                                     }
                                                 });
            changesLabel.setLabelFor(changesList);
            changesLabel.setDisplayedMnemonic(Util.getString("LAB_ChangesList_Mnemonic").charAt(0));

            jScrollPane1.setViewportView (changesList);

            changesPanel.add (jScrollPane1, "Center"); // NOI18N

            add (changesPanel, "Center"); // NOI18N

            radioPanel = new JPanel ();
            radioPanel.setLayout (new java.awt.GridLayout (3, 1));
            radioPanel.setBorder(new CompoundBorder(
                                     new TitledBorder(Util.getString("LAB_SynchMode")),
                                     new EmptyBorder(5, 5, 5, 5))
                                );

            radioDisable = new JRadioButton ();
            radioDisable.setText (Util.getString("LAB_radioDisable"));
            radioDisable.setMnemonic(Util.getString("LAB_radioDisable_Mnemonic").charAt(0));
            radioDisable.setToolTipText(Util.getString("ACS_radioDisableA11yDesc"));

            radioPanel.add (radioDisable);

            radioConfirm = new JRadioButton ();
            radioConfirm.setText (Util.getString("LAB_radioConfirm"));
            radioConfirm.setMnemonic(Util.getString("LAB_radioConfirm_Mnemonic").charAt(0));
            radioConfirm.setToolTipText(Util.getString("ACS_radioConfirmA11yDesc"));

            radioPanel.add (radioConfirm);

            radioAuto = new JRadioButton ();
            radioAuto.setText (Util.getString("LAB_radioAuto"));
            radioAuto.setMnemonic(Util.getString("LAB_radioAuto_Mnemonic").charAt(0));
            radioAuto.setToolTipText(Util.getString("ACS_radioAutoA11yDesc"));

            radioPanel.add (radioAuto);

            ButtonGroup group = new ButtonGroup();
            group.add(radioDisable);
            group.add(radioConfirm);
            group.add(radioAuto);

            add (radioPanel, "South"); // NOI18N
        }
        
        public java.awt.Dimension getPreferredSize() {
            java.awt.Dimension size = super.getPreferredSize();
            size.width = Math.max(600, Math.min(size.width, java.awt.Toolkit.getDefaultToolkit().getScreenSize().width - 200));
            size.height = Math.max(300, Math.min(size.height, java.awt.Toolkit.getDefaultToolkit().getScreenSize().width - 200));
            return size;
        }

        byte getRadio() {
            if (radioDisable.isSelected())
                return JavaDataObject.CONNECT_NOT;
            else if (radioConfirm.isSelected())
                return JavaDataObject.CONNECT_CONFIRM;
            else
                return JavaDataObject.CONNECT_AUTO;
        }

        void setRadio(byte button) {
            switch (button) {
            case JavaDataObject.CONNECT_NOT:
                radioDisable.setSelected(true);
                break;
            case JavaDataObject.CONNECT_CONFIRM:
                radioConfirm.setSelected(true);
                break;
            case JavaDataObject.CONNECT_AUTO:
                radioAuto.setSelected(true);
                break;
            }
        }

        synchronized void setChanges(List changes) {
            listModel.clear();
            if (changes != null) {
                Iterator it = changes.iterator();
                while (it.hasNext())
                    listModel.addElement(it.next());
            }
        }
    }

    static class ChangesListCellRenderer extends DefaultListCellRenderer {

        static final long serialVersionUID =-8439520404877315783L;

        public Component getListCellRendererComponent(JList list, Object value,
                int index, boolean isSelected,
                boolean cellHasFocus) {
            Component comp = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
            if ((comp instanceof JLabel) && (value instanceof ChangeProcessor)) {
                ((JLabel)comp).setText(((ChangeProcessor)value).getDisplayName());
            }
            return comp;
        }
    }
}

... this post is sponsored by my books ...

#1 New Release!

FP Best Seller

 

new blog posts

 

Copyright 1998-2021 Alvin Alexander, alvinalexander.com
All Rights Reserved.

A percentage of advertising revenue from
pages under the /java/jwarehouse URI on this website is
paid back to open source projects.