Java tutorial
/* * Copyright (c) 2008, SQL Power Group Inc. * * This file is part of Power*Architect. * * Power*Architect is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * Power*Architect is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package ca.sqlpower.architect.swingui; import java.awt.Component; import java.awt.Font; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; import java.beans.PropertyChangeEvent; import java.sql.DatabaseMetaData; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSpinner; import javax.swing.JTextArea; import javax.swing.JTextField; import javax.swing.JTree; import javax.swing.SpinnerNumberModel; import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.event.AncestorEvent; import javax.swing.event.AncestorListener; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.text.JTextComponent; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.TreeNode; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; import org.apache.log4j.Logger; import ca.sqlpower.architect.ddl.DDLUtils; import ca.sqlpower.architect.swingui.dbtree.DBTreeCellRenderer; import ca.sqlpower.architect.swingui.dbtree.DBTreeModel; import ca.sqlpower.object.AbstractPoolingSPListener; import ca.sqlpower.object.SPChildEvent; import ca.sqlpower.object.SPListener; import ca.sqlpower.sqlobject.SQLColumn; import ca.sqlpower.sqlobject.SQLObject; import ca.sqlpower.sqlobject.SQLObjectException; import ca.sqlpower.sqlobject.SQLObjectUtils; import ca.sqlpower.sqlobject.SQLTypePhysicalPropertiesProvider; import ca.sqlpower.sqlobject.SQLTypePhysicalPropertiesProvider.PropertyType; import ca.sqlpower.sqlobject.UserDefinedSQLType; import ca.sqlpower.swingui.ChangeListeningDataEntryPanel; import ca.sqlpower.swingui.DataEntryPanelChangeUtil; import ca.sqlpower.swingui.PopupJTreeAction; import ca.sqlpower.swingui.SPSUtils; import ca.sqlpower.util.SQLPowerUtils; import ca.sqlpower.util.TransactionEvent; import com.jgoodies.forms.layout.CellConstraints; import com.jgoodies.forms.layout.FormLayout; import com.jgoodies.forms.layout.RowSpec; /** * A DataEntryPanel implementation that is capable of modifying the properties * of one or more columns. The user interface is slightly different in multi-column * edit mode. */ public class ColumnEditPanel extends ChangeListeningDataEntryPanel implements ActionListener, SPListener { private static final Logger logger = Logger.getLogger(ColumnEditPanel.class); private static final Font TITLE_FONT = UIManager.getFont("Label.font").deriveFont(Font.BOLD, 10f); /** * A simple enum that gives a nicer name to true and false for combo boxes. * <p> * Used in testing */ static enum YesNoEnum { YES("Yes", true), NO("No", false); private final String displayName; private final boolean value; private YesNoEnum(String displayName, boolean value) { this.displayName = displayName; this.value = value; } @Override public String toString() { return displayName; } public boolean getValue() { return value; } public static YesNoEnum valueOf(Boolean bool) { if (bool == null) { return null; } else if (bool) { return YES; } else { return NO; } } } /** * The column we're editing. */ private final List<SQLColumn> columns; private final JPanel panel; /** * Mapping of data entry components to the checkboxes that say whether * or not the value should be applied. */ private final Map<JComponent, JCheckBox> componentEnabledMap = new HashMap<JComponent, JCheckBox>(); /** * Mapping of data entry components specific to data types to the check * boxes that say if the value in the column editor window should override * the value from the underlying type or if the underlying type should be * used instead. */ private final Map<JComponent, JCheckBox> typeOverrideMap = new HashMap<JComponent, JCheckBox>(); /** * Label that shows where the column was reverse engineered from, or * where its data comes from when building an ETL mapping. */ private final JButton colSourceButton; private final JTree colSourceTree; private final TreeNode sourceNotSpecifiedTreeNode = new DefaultMutableTreeNode( Messages.getString("ColumnEditPanel.noneSpecified"), false); private final JTextField colLogicalName; private final JTextField colPhysicalName; private final JButton typeChooserButton; private final JTree colType; private final JSpinner colScale; private final JSpinner colPrec; private final JComboBox colNullable; private final JTextArea colRemarks; private final JTextField colDefaultValue; private final JCheckBox colInPK; private final JComboBox colAutoInc; private final JCheckBox colPrecCB; private final JCheckBox colScaleCB; /** * Text field for the name of the sequence that will generate this column's * default values. In multi-edit mode, this component will be null. */ private final JTextField colAutoIncSequenceName; /** * The prefix string that comes before the current column name in the * sequence name. This is set via the {@link #discoverSequenceNamePattern()} * method, which should be called automatically whenever the user changes * the sequence name. */ private String seqNamePrefix; /** * The suffix string that comes after the current column name in the * sequence name. This is set via the {@link #discoverSequenceNamePattern()} * method, which should be called automatically whenever the user changes * the sequence name. */ private String seqNameSuffix; private final ArchitectSwingSession session; public ColumnEditPanel(SQLColumn col, ArchitectSwingSession session) throws SQLObjectException { this(Collections.singleton(col), session); } public ColumnEditPanel(Collection<SQLColumn> cols, final ArchitectSwingSession session) throws SQLObjectException { logger.debug("ColumnEditPanel called"); //$NON-NLS-1$ if (session == null) { throw new NullPointerException("Null session is not allowed"); //$NON-NLS-1$ } this.session = session; if (cols == null || cols.isEmpty()) { throw new NullPointerException("Null or empty collection of columns is not allowed"); //$NON-NLS-1$ } columns = new ArrayList<SQLColumn>(cols); // if (columns.get(0).getParent() != null) { // columns.get(0).getParent().getPrimaryKeyIndex().addSPListener(this); // for (SQLColumn col : columns) { // col.addSPListener(this); // } // } FormLayout layout = new FormLayout("pref, pref, pref:grow, 4dlu, pref, pref:grow", ""); layout.setColumnGroups(new int[][] { { 3, 6 } }); panel = new JPanel(layout); CellConstraints cc = new CellConstraints(); JCheckBox cb; int row = 1; int width = 5; layout.appendRow(RowSpec.decode("p")); panel.add(makeTitle(Messages.getString("ColumnEditPanel.source")), cc.xyw(2, row++, width)); //$NON-NLS-1$ layout.appendRow(RowSpec.decode("p")); cb = new JCheckBox(); if (cols.size() > 1) { panel.add(cb, cc.xy(1, row)); } colSourceTree = new JTree(); DBTreeModel sourceTreeModel = new DBTreeModel(session.getRootObject(), colSourceTree, false, true, false, false, false) { @Override public Object getChild(Object parent, int index) { if (parent == sourceNotSpecifiedTreeNode) { return null; } else if (parent == getRoot()) { if (index == 0) { return sourceNotSpecifiedTreeNode; } else { return super.getChild(parent, index - 1); } } else { return super.getChild(parent, index); } } @Override public int getChildCount(Object parent) { if (parent == sourceNotSpecifiedTreeNode) { return 0; } else if (parent == getRoot()) { return super.getChildCount(parent) + 1; } else { return super.getChildCount(parent); } } @Override public int getIndexOfChild(Object parent, Object child) { if (parent == sourceNotSpecifiedTreeNode) { return -1; } else if (child == sourceNotSpecifiedTreeNode) { return 0; } else if (parent == getRoot()) { int index = super.getIndexOfChild(parent, child); if (index != -1) { return index + 1; } else { return -1; } } else { return super.getIndexOfChild(parent, child); } } @Override public boolean isLeaf(Object parent) { if (parent == sourceNotSpecifiedTreeNode) { return true; } else { return super.isLeaf(parent); } } }; colSourceTree.setModel(sourceTreeModel); colSourceTree.setRootVisible(false); colSourceTree.setShowsRootHandles(true); colSourceTree.setCellRenderer(new DBTreeCellRenderer() { @Override public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) { super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); if (!sel && value == sourceNotSpecifiedTreeNode) { setForeground(getTextNonSelectionColor()); } return this; } }); colSourceTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); colSourceButton = new JButton(); colSourceButton.setAction(new PopupJTreeAction(panel, colSourceTree, colSourceButton, SQLColumn.class)); panel.add(colSourceButton, cc.xyw(2, row++, 2)); componentEnabledMap.put(colSourceTree, cb); layout.appendRow(RowSpec.decode("5dlu")); row++; layout.appendRow(RowSpec.decode("p")); panel.add(makeTitle(Messages.getString("ColumnEditPanel.logicalName")), cc.xyw(2, row++, width)); //$NON-NLS-1$ layout.appendRow(RowSpec.decode("p")); cb = new JCheckBox(); if (cols.size() > 1) { panel.add(cb, cc.xy(1, row)); } panel.add(colLogicalName = new JTextField(), cc.xyw(2, row++, width)); componentEnabledMap.put(colLogicalName, cb); colLogicalName.getDocument().addDocumentListener(new DocumentCheckboxEnabler(cb)); colLogicalName.addComponentListener(new ComponentAdapter() { @Override public void componentShown(ComponentEvent e) { colLogicalName.requestFocusInWindow(); } }); colLogicalName.addFocusListener(new FocusAdapter() { public void focusGained(FocusEvent e) { if (logger.isDebugEnabled()) { logger.debug("focus Gained : " + e); } colLogicalName.selectAll(); } }); layout.appendRow(RowSpec.decode("5dlu")); row++; layout.appendRow(RowSpec.decode("p")); panel.add(makeTitle(Messages.getString("ColumnEditPanel.physicalName")), cc.xyw(2, row++, width)); //$NON-NLS-1$ layout.appendRow(RowSpec.decode("p")); cb = new JCheckBox(); if (cols.size() > 1) { panel.add(cb, cc.xy(1, row)); } panel.add(colPhysicalName = new JTextField(), cc.xyw(2, row++, width)); componentEnabledMap.put(colPhysicalName, cb); colPhysicalName.getDocument().addDocumentListener(new DocumentCheckboxEnabler(cb)); colPhysicalName.addComponentListener(new ComponentAdapter() { @Override public void componentShown(ComponentEvent e) { colPhysicalName.requestFocusInWindow(); } }); colPhysicalName.addFocusListener(new FocusAdapter() { public void focusGained(FocusEvent e) { if (logger.isDebugEnabled()) { logger.debug("focus Gained : " + e); } colPhysicalName.selectAll(); } }); layout.appendRow(RowSpec.decode("5dlu")); row++; layout.appendRow(RowSpec.decode("p")); cb = new JCheckBox(); if (cols.size() > 1) { panel.add(cb, cc.xy(1, row)); } panel.add(colInPK = new JCheckBox(Messages.getString("ColumnEditPanel.inPrimaryKey")), //$NON-NLS-1$ cc.xyw(2, row++, width)); componentEnabledMap.put(colInPK, cb); colInPK.addActionListener(this); colInPK.addActionListener(checkboxEnabler); layout.appendRow(RowSpec.decode("5dlu")); row++; layout.appendRow(RowSpec.decode("p")); panel.add(makeTitle(Messages.getString("ColumnEditPanel.type")), cc.xyw(2, row++, width)); //$NON-NLS-1$ layout.appendRow(RowSpec.decode("p")); cb = new JCheckBox(); if (cols.size() > 1) { panel.add(cb, cc.xy(1, row)); } typeChooserButton = new JButton(Messages.getString("ColumnEditPanel.chooseType")); if (session.isEnterpriseSession()) { colType = new JTree(new SQLTypeTreeModel(session.getEnterpriseSession())); } else { colType = new JTree(new SQLTypeTreeModel(session)); } colType.setCellRenderer(new SQLTypeTreeCellRenderer()); for (int i = 0; i < colType.getRowCount(); i++) { colType.expandRow(i); } colType.setRootVisible(true); colType.setShowsRootHandles(true); colType.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); typeChooserButton .setAction(new PopupJTreeAction(panel, colType, typeChooserButton, UserDefinedSQLType.class)); componentEnabledMap.put(colType, cb); panel.add(typeChooserButton, cc.xyw(2, row++, 2)); layout.appendRow(RowSpec.decode("5dlu")); row++; layout.appendRow(RowSpec.decode("p")); panel.add(makeTitle(Messages.getString("ColumnEditPanel.precision")), cc.xy(3, row)); //$NON-NLS-1$ panel.add(makeTitle(Messages.getString("ColumnEditPanel.scale")), cc.xy(6, row++)); //$NON-NLS-1$ layout.appendRow(RowSpec.decode("p")); panel.add(colPrec = createPrecisionEditor(), cc.xy(3, row)); colPrec.addChangeListener(checkboxEnabler); SPSUtils.makeJSpinnerSelectAllTextOnFocus(colPrec); colPrecCB = new JCheckBox(); panel.add(colPrecCB, cc.xy(2, row)); typeOverrideMap.put(colPrec, colPrecCB); colPrecCB.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent e) { if (colPrecCB.isSelected()) { colPrec.setEnabled(true); } else { colPrec.setEnabled(false); if (colType.getLastSelectedPathComponent() instanceof UserDefinedSQLType) { colPrec.setValue(((UserDefinedSQLType) colType.getLastSelectedPathComponent()) .getPrecision(SQLTypePhysicalPropertiesProvider.GENERIC_PLATFORM)); } } } }); colPrec.setEnabled(false); colScaleCB = new JCheckBox(); panel.add(colScaleCB, cc.xy(5, row)); panel.add(colScale = createScaleEditor(), cc.xy(6, row++)); typeOverrideMap.put(colScale, colScaleCB); colScale.addChangeListener(checkboxEnabler); SPSUtils.makeJSpinnerSelectAllTextOnFocus(colScale); colScaleCB.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent e) { if (colScaleCB.isSelected()) { colScale.setEnabled(true); } else { colScale.setEnabled(false); if (colType.getLastSelectedPathComponent() instanceof UserDefinedSQLType) { colScale.setValue(((UserDefinedSQLType) colType.getLastSelectedPathComponent()) .getScale(SQLTypePhysicalPropertiesProvider.GENERIC_PLATFORM)); } } } }); colScale.setEnabled(false); layout.appendRow(RowSpec.decode("5dlu")); row++; layout.appendRow(RowSpec.decode("p")); panel.add(makeTitle(Messages.getString("ColumnEditPanel.allowsNulls")), cc.xyw(3, row++, width - 1)); //$NON-NLS-1$ layout.appendRow(RowSpec.decode("p")); final JCheckBox colNullCB = new JCheckBox(); panel.add(colNullCB, cc.xy(2, row)); panel.add(colNullable = new JComboBox(YesNoEnum.values()), cc.xy(3, row++)); //$NON-NLS-1$ typeOverrideMap.put(colNullable, colNullCB); colNullable.addActionListener(this); colNullable.addActionListener(checkboxEnabler); colNullCB.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent e) { if (colNullCB.isSelected()) { colNullable.setEnabled(true); } else { colNullable.setEnabled(false); if (colType.getLastSelectedPathComponent() instanceof UserDefinedSQLType) { colNullable.setSelectedItem( YesNoEnum.valueOf(((UserDefinedSQLType) colType.getLastSelectedPathComponent()) .getNullability() == DatabaseMetaData.columnNullable)); } } updateComponents(); } }); colNullable.setEnabled(false); layout.appendRow(RowSpec.decode("3dlu")); row++; layout.appendRow(RowSpec.decode("p")); panel.add(makeTitle(Messages.getString("ColumnEditPanel.autoIncrement")), cc.xyw(3, row++, width - 1)); //$NON-NLS-1$ layout.appendRow(RowSpec.decode("p")); final JCheckBox colAutoIncCB = new JCheckBox(); panel.add(colAutoIncCB, cc.xy(2, row)); panel.add(colAutoInc = new JComboBox(YesNoEnum.values()), cc.xy(3, row++)); //$NON-NLS-1$ typeOverrideMap.put(colAutoInc, colAutoIncCB); colAutoInc.addActionListener(this); colAutoInc.addActionListener(checkboxEnabler); colAutoIncCB.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent e) { if (colAutoIncCB.isSelected()) { colAutoInc.setEnabled(true); } else { colAutoInc.setEnabled(false); if (colType.getLastSelectedPathComponent() instanceof UserDefinedSQLType) { colAutoInc.setSelectedItem(YesNoEnum.valueOf( ((UserDefinedSQLType) colType.getLastSelectedPathComponent()).getAutoIncrement())); } } } }); colAutoInc.setEnabled(false); layout.appendRow(RowSpec.decode("5dlu")); row++; layout.appendRow(RowSpec.decode("p")); panel.add(makeTitle(Messages.getString("ColumnEditPanel.defaultValue")), cc.xyw(3, row++, width - 1)); //$NON-NLS-1$ layout.appendRow(RowSpec.decode("p")); final JCheckBox colDefaultCB = new JCheckBox(); panel.add(colDefaultCB, cc.xy(2, row)); panel.add(colDefaultValue = new JTextField(), cc.xyw(3, row++, width - 1)); colDefaultValue.setEnabled(false); typeOverrideMap.put(colDefaultValue, colDefaultCB); colDefaultValue.addActionListener(this); colDefaultCB.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent e) { if (colDefaultCB.isSelected()) { colDefaultValue.setEnabled(true); } else { colDefaultValue.setEnabled(false); if (colType.getLastSelectedPathComponent() instanceof UserDefinedSQLType) { colDefaultValue.setText(((UserDefinedSQLType) colType.getLastSelectedPathComponent()) .getDefaultValue(SQLTypePhysicalPropertiesProvider.GENERIC_PLATFORM)); } } updateComponents(); } }); layout.appendRow(RowSpec.decode("6dlu")); row++; layout.appendRow(RowSpec.decode("p")); panel.add(makeTitle(Messages.getString("ColumnEditPanel.sequenceName")), cc.xyw(2, row++, width)); //$NON-NLS-1$ layout.appendRow(RowSpec.decode("p")); cb = new JCheckBox(); if (cols.size() > 1) { panel.add(cb, cc.xy(1, row)); } panel.add(colAutoIncSequenceName = new JTextField(), cc.xyw(2, row++, width)); componentEnabledMap.put(colAutoIncSequenceName, cb); colAutoIncSequenceName.getDocument().addDocumentListener(new DocumentCheckboxEnabler(cb)); DocumentListener listener = new DocumentListener() { public void changedUpdate(DocumentEvent e) { syncSequenceName(); } public void insertUpdate(DocumentEvent e) { syncSequenceName(); } public void removeUpdate(DocumentEvent e) { syncSequenceName(); } }; // Listener to update the sequence name when the column name changes colPhysicalName.getDocument().addDocumentListener(listener); colLogicalName.getDocument().addDocumentListener(listener); // Listener to rediscover the sequence naming convention, and reset the // sequence name to its original (according to the column's own sequence // name) naming convention when the user clears the sequence name field colAutoIncSequenceName.addFocusListener(new FocusAdapter() { @Override public void focusLost(FocusEvent e) { if (columns.size() == 1 && colAutoIncSequenceName.getText().trim().equals("")) { //$NON-NLS-1$ // Changing sequence name doesn't make sense in multi-edit // because sequence names have to be unique SQLColumn column = columns.iterator().next(); if (column.getPhysicalName() != null && !column.getPhysicalName().trim().equals("")) { discoverSequenceNamePattern(column.getPhysicalName()); } else { discoverSequenceNamePattern(column.getName()); } syncSequenceName(); } else { if (colPhysicalName.getText() != null && !colPhysicalName.getText().trim().equals("")) { discoverSequenceNamePattern(colPhysicalName.getText()); } else { discoverSequenceNamePattern(colLogicalName.getText()); } } } }); layout.appendRow(RowSpec.decode("5dlu")); row++; layout.appendRow(RowSpec.decode("p")); panel.add(makeTitle(Messages.getString("ColumnEditPanel.remarks")), cc.xyw(2, row++, width)); //$NON-NLS-1$ layout.appendRow(RowSpec.decode("pref:grow")); cb = new JCheckBox(); if (cols.size() > 1) { panel.add(cb, cc.xy(1, row, "center, top")); } panel.add(new JScrollPane(colRemarks = new JTextArea()), cc.xyw(2, row++, width, "fill, fill")); componentEnabledMap.put(colRemarks, cb); colRemarks.getDocument().addDocumentListener(new DocumentCheckboxEnabler(cb)); colRemarks.setRows(5); colRemarks.setLineWrap(true); colRemarks.setWrapStyleWord(true); // start with all components enabled; if there are multiple columns // to edit, these checkboxes will be turned off selectively for the // mismatching values for (JCheckBox checkbox : componentEnabledMap.values()) { checkbox.setSelected(true); } //The type covers multiple fields and needs a different check to see if //it should start enabled. All type info must match across the objects //for the checkbox to start selected if (cols.size() > 1) { Iterator<SQLColumn> colIterator = cols.iterator(); SQLColumn firstCol = colIterator.next(); while (colIterator.hasNext()) { SQLColumn nextCol = colIterator.next(); if (!firstCol.getTypeName().equals(nextCol.getTypeName()) || firstCol.getPrecision() != nextCol.getPrecision() || firstCol.getScale() != nextCol.getScale() || firstCol.getNullable() != nextCol.getNullable() || firstCol.isAutoIncrement() != nextCol.isAutoIncrement() || !firstCol.getDefaultValue().equals(nextCol.getDefaultValue())) { componentEnabledMap.get(colType).setSelected(false); break; } } } for (SQLColumn col : cols) { logger.debug("Updating component state for column " + col); updateComponents(col); } // TODO only give focus to column name if it's enabled? colPhysicalName.requestFocus(); colPhysicalName.selectAll(); SQLPowerUtils.listenToHierarchy(session.getRootObject(), obsolesenceListener); SQLPowerUtils.listenToHierarchy(session.getRootObject(), this); panel.addAncestorListener(cleanupListener); colSourceTree.addTreeSelectionListener(new TreeSelectionListener() { @Override public void valueChanged(TreeSelectionEvent e) { TreePath path = e.getNewLeadSelectionPath(); if (path != null) { Object selection = path.getLastPathComponent(); if (selection instanceof SQLColumn) { SQLColumn sourceColumn = (SQLColumn) selection; colSourceButton.setText( DDLUtils.toQualifiedName(sourceColumn.getParent()) + "." + sourceColumn.getName()); } else { colSourceButton.setText(Messages.getString("ColumnEditPanel.noneSpecified")); } } else { colSourceButton.setText(Messages.getString("ColumnEditPanel.noneSpecified")); } } }); colType.addTreeSelectionListener(new TreeSelectionListener() { public void valueChanged(TreeSelectionEvent e) { TreePath path = e.getNewLeadSelectionPath(); if (path != null) { Object selection = path.getLastPathComponent(); if (selection instanceof UserDefinedSQLType) { typeChooserButton.setText(((UserDefinedSQLType) selection).getName()); updateSQLTypeComponents((UserDefinedSQLType) selection, false); } else { typeChooserButton.setText(Messages.getString("ColumnEditPanel.chooseType")); } } else { typeChooserButton.setText(Messages.getString("ColumnEditPanel.chooseType")); } } }); } private Component makeTitle(String string) { JLabel label = new JLabel(string); label.setFont(TITLE_FONT); return label; } private JSpinner createScaleEditor() { return new JSpinner(new SpinnerNumberModel(0, 0, Integer.MAX_VALUE, 1)); } private JSpinner createPrecisionEditor() { return createScaleEditor(); // looks better if both spinners are same // size } /** * Updates all the UI components to reflect the given column's properties. * <p> * This is a constructor subroutine which is only called one time per * instance per column being edited. Once a ColumnEditPanel is constructed, * it is forever tied to the column or columns it was constructed with. In * the multi-column-edit case, the first call to this method works "as usual," * meaning that all the fields get set to represent the values of that column. * Subsequent calls end up unchecking the "apply this value" checkboxes beside * each component whenever a difference is discovered between the component's * existing value and the value that would have been set for that subsequent * column. * * @param col One of the columns to edit in this dialog. */ private void updateComponents(SQLColumn col) throws SQLObjectException { SQLColumn sourceColumn = col.getSourceColumn(); if (sourceColumn == null) { Object[] treePath = { session.getRootObject(), sourceNotSpecifiedTreeNode }; colSourceTree.setSelectionPath(new TreePath(treePath)); colSourceButton.setText(Messages.getString("ColumnEditPanel.noneSpecified")); //$NON-NLS-1$ } else { updateComponent(colSourceTree, sourceColumn); DBTreeModel model = (DBTreeModel) colSourceTree.getModel(); colSourceTree.setSelectionPath(new TreePath(model.getPathToNode(sourceColumn))); colSourceButton .setText(DDLUtils.toQualifiedName(sourceColumn.getParent()) + "." + sourceColumn.getName()); } updateComponent(colLogicalName, col.getName()); updateComponent(colPhysicalName, col.getPhysicalName()); updateComponent(colType, col.getUserDefinedSQLType().getUpstreamType()); if (!colType.isSelectionEmpty()) { typeChooserButton .setText(((UserDefinedSQLType) col.getUserDefinedSQLType().getUpstreamType()).getName()); } else { colSourceButton.setText(Messages.getString("ColumnEditPanel.noneSpecified")); } updateSQLTypeComponents(col.getUserDefinedSQLType(), true); updateComponent(colRemarks, col.getRemarks()); boolean inPk; if (col.getParent() == null) { inPk = SQLColumn.isDefaultInPK(); // XXX looks fishy--how can a column be in the PK if it has no parent table? logger.debug("new constructed column"); } else { inPk = col.isPrimaryKey(); logger.debug("existing column"); } updateComponent(colInPK, inPk); logger.debug("Selected" + colInPK.isSelected()); updateComponent(colAutoIncSequenceName, col.getAutoIncrementSequenceName()); updateComponents(); if (col.getPhysicalName() != null && !col.getPhysicalName().trim().equals("")) { discoverSequenceNamePattern(col.getPhysicalName()); } else { discoverSequenceNamePattern(col.getName()); } } private void updateComponent(JTree comp, Object expectedValue) { if (componentEnabledMap.get(comp).isSelected() && (comp.isSelectionEmpty() || comp.getLastSelectedPathComponent() == expectedValue)) { for (int i = 0; i < comp.getRowCount(); i++) { Object lastPathComponent = comp.getPathForRow(i).getLastPathComponent(); if (lastPathComponent == expectedValue) { comp.setSelectionRow(i); } } } else { comp.clearSelection(); componentEnabledMap.get(comp).setSelected(false); } } /** Subroutine of {@link #updateComponents(SQLColumn)}. */ private void updateComponent(JTextComponent comp, String expectedValue) { boolean unvisited = comp.getText().equals(""); if (componentEnabledMap.get(comp).isSelected() && (unvisited || comp.getText().equals(expectedValue))) { comp.setText(expectedValue); } else { comp.setText(""); componentEnabledMap.get(comp).setSelected(false); } } /** Subroutine of {@link #updateComponents(SQLColumn)}. */ private void updateComponent(JCheckBox comp, boolean expectedValue) { // Checking if a checkbox was visited is not possible just by examining its value, // so we check for (and store) a client property when we visit it final String multiEditVisitedProperty = "ColumnEditPanel.multiEditVisited"; boolean unvisited = comp.getClientProperty(multiEditVisitedProperty) == null; if (componentEnabledMap.get(comp).isSelected() && (unvisited || comp.isSelected() == expectedValue)) { comp.setSelected(expectedValue); } else { comp.setSelected(false); componentEnabledMap.get(comp).setSelected(false); } comp.putClientProperty(multiEditVisitedProperty, Boolean.TRUE); } /** * Figures out what the sequence name prefix and suffix strings are, based * on the current contents of the sequence name and column name fields. */ private void discoverSequenceNamePattern(String colName) { String seqName = colAutoIncSequenceName.getText(); int prefixEnd = seqName.indexOf(colName); String tableName = null; if (columns.get(0).getParent() != null) { tableName = columns.get(0).getParent().getPhysicalName(); } if ((prefixEnd != -1 && seqName.substring(prefixEnd + colName.length()).indexOf(colName) == -1)) { seqNamePrefix = seqName.substring(0, prefixEnd); seqNameSuffix = seqName.substring(prefixEnd + colName.length()); } else if (seqName.equals(tableName + "_" + colName + "_seq")) { seqNamePrefix = tableName + "_"; seqNameSuffix = "_seq"; } else { seqNamePrefix = null; seqNameSuffix = null; } } /** * Modifies the contents of the "auto-increment sequence name" field to * match the naming scheme as it is currently understood. This modification * is only performed if the naming scheme has been successfully determined * by the {@link #discoverSequenceNamePattern(String)} method. The new * sequence name is written directly to the {@link #colAutoIncSequenceName} * field. */ private void syncSequenceName() { if (seqNamePrefix != null && seqNameSuffix != null) { String newName = seqNamePrefix; newName += (colPhysicalName.getText() == null || colPhysicalName.getText().trim().equals("")) ? colLogicalName.getText() : colPhysicalName.getText(); newName += seqNameSuffix; colAutoIncSequenceName.setText(newName); } } /** * Implementation of ActionListener. */ public void actionPerformed(ActionEvent e) { logger.debug("action event " + e); //$NON-NLS-1$ updateComponents(); } /** * Examines the components and makes sure they're in a consistent state * (they are legal with respect to the model). */ private void updateComponents() { // allow nulls is free unless column is in PK if (colInPK.isSelected() || !typeOverrideMap.get(colNullable).isSelected()) { colNullable.setEnabled(false); } else { colNullable.setEnabled(true); } // primary key is free unless column allows nulls if (((YesNoEnum) colNullable.getSelectedItem()).getValue()) { colInPK.setEnabled(false); } else { colInPK.setEnabled(true); } if (colInPK.isSelected() && ((YesNoEnum) colNullable.getSelectedItem()).getValue()) { // this should not be physically possible colNullable.setSelectedItem(false); colNullable.setEnabled(false); } if (colAutoInc.getSelectedIndex() == -1 || ((YesNoEnum) colAutoInc.getSelectedItem()).getValue() || !typeOverrideMap.get(colDefaultValue).isSelected()) { colDefaultValue.setText(""); //$NON-NLS-1$ colDefaultValue.setEnabled(false); } else { colDefaultValue.setEnabled(true); } if (colAutoInc.getSelectedIndex() != -1) { colAutoIncSequenceName.setEnabled(((YesNoEnum) colAutoInc.getSelectedItem()).getValue()); } else { colAutoIncSequenceName.setEnabled(false); } } /** * Sets the properties of each column being edited to match those on screen. Only * components with their associated checkbox selected will be considered. * * @return A list of error messages if the update was not successful. */ private List<String> updateModel() { logger.debug("Updating model"); //$NON-NLS-1$ List<String> errors = new ArrayList<String>(); if (componentEnabledMap.get(colType).isSelected() && !(colType.getLastSelectedPathComponent() instanceof UserDefinedSQLType)) { errors.add(Messages.getString("ColumnEditPanel.missingType")); return errors; } SQLObject compoundEditRoot = SQLObjectUtils.findCommonAncestor(columns); logger.debug("Compound edit root is " + compoundEditRoot); try { compoundEditRoot.begin(Messages.getString("ColumnEditPanel.compoundEditName")); //$NON-NLS-1$ for (SQLColumn column : columns) { if (componentEnabledMap.get(colSourceTree).isSelected()) { Object selection = colSourceTree.getLastSelectedPathComponent(); if (selection instanceof SQLColumn) { column.setSourceColumn((SQLColumn) selection); } else { column.setSourceColumn(null); } } if (componentEnabledMap.get(colPhysicalName).isSelected()) { column.setPhysicalName(colPhysicalName.getText()); } if (componentEnabledMap.get(colLogicalName).isSelected()) { if (colLogicalName.getText().trim().length() == 0) { errors.add(Messages.getString("ColumnEditPanel.columnNameRequired")); //$NON-NLS-1$ } else { column.setName(colLogicalName.getText()); } } if (componentEnabledMap.get(colType).isSelected()) { // Set upstream type on column UserDefinedSQLType upstreamType = (UserDefinedSQLType) colType.getLastSelectedPathComponent(); column.getUserDefinedSQLType().setUpstreamType(upstreamType); // Set scale if (typeOverrideMap.get(colScale).isSelected()) { column.setScale(((Integer) colScale.getValue()).intValue()); } else { column.getUserDefinedSQLType().setScale(SQLTypePhysicalPropertiesProvider.GENERIC_PLATFORM, null); } // Set precision if (typeOverrideMap.get(colPrec).isSelected()) { column.setPrecision(((Integer) colPrec.getValue()).intValue()); } else { column.getUserDefinedSQLType() .setPrecision(SQLTypePhysicalPropertiesProvider.GENERIC_PLATFORM, null); } // Set nullability if (typeOverrideMap.get(colNullable).isSelected()) { column.setNullable(((YesNoEnum) colNullable.getSelectedItem()).getValue() ? DatabaseMetaData.columnNullable : DatabaseMetaData.columnNoNulls); } else { column.getUserDefinedSQLType().setMyNullability(null); } if (typeOverrideMap.get(colDefaultValue).isSelected()) { // avoid setting default value to empty string if (!(column.getDefaultValue() == null && colDefaultValue.getText().equals(""))) { //$NON-NLS-1$ column.setDefaultValue(colDefaultValue.getText()); } } else { column.getUserDefinedSQLType() .setDefaultValue(SQLTypePhysicalPropertiesProvider.GENERIC_PLATFORM, null); } // Autoincrement has to go before the primary key or // this column will never allow nulls if (typeOverrideMap.get(colAutoInc).isSelected()) { column.setAutoIncrement(((YesNoEnum) colAutoInc.getSelectedItem()).getValue()); } else { column.getUserDefinedSQLType().setMyAutoIncrement(null); } } if (componentEnabledMap.get(colRemarks).isSelected()) { column.setRemarks(colRemarks.getText()); } if (componentEnabledMap.get(colInPK).isSelected()) { if (colInPK.isSelected() && !column.isPrimaryKey()) { column.getParent().addToPK(column); } else if (!colInPK.isSelected() && column.isPrimaryKey()) { column.getParent().moveAfterPK(column); } } if (componentEnabledMap.get(colAutoIncSequenceName).isSelected()) { column.setAutoIncrementSequenceName(colAutoIncSequenceName.getText()); if (colAutoIncSequenceName.getText().equals("")) { column.setAutoIncrementSequenceName(column.makeAutoIncrementSequenceName()); } } } } catch (SQLObjectException e) { throw new RuntimeException(e); } finally { compoundEditRoot.commit(); } return errors; } // ------------------ ARCHITECT PANEL INTERFACE --------------------- /** * Calls updateModel since the user may have clicked "ok" before hitting * enter on a text field. */ public boolean applyChanges() { SQLPowerUtils.unlistenToHierarchy(session.getRootObject(), this); List<String> errors = updateModel(); if (!errors.isEmpty()) { StringBuffer buffer = new StringBuffer(); buffer.append("<html>"); for (String error : errors) { buffer.append(error); buffer.append("<br>"); } buffer.append("</html>"); JOptionPane.showMessageDialog(panel, buffer.toString(), Messages.getString("ColumnEditPanel.errorTitle"), JOptionPane.WARNING_MESSAGE); return false; } else { return true; } } /** * Does nothing. The column's properties will not have been modified. */ public void discardChanges() { SQLPowerUtils.unlistenToHierarchy(session.getRootObject(), this); } /* docs inherit from interface */ public JPanel getPanel() { return panel; } /** Only for testing. Normal client code should not need to call this. */ public JComboBox getColAutoInc() { return colAutoInc; } /** Only for testing. Normal client code should not need to call this. */ public JTextField getColDefaultValue() { return colDefaultValue; } /** Only for testing. Normal client code should not need to call this. */ public JCheckBox getColInPK() { return colInPK; } /** Only for testing. Normal client code should not need to call this. */ public JTextField getColLogicalName() { return colLogicalName; } /** Only for testing. Normal client code should not need to call this. */ public JTextField getColPhysicalName() { return colPhysicalName; } /** Only for testing. Normal client code should not need to call this. */ public JComboBox getColNullable() { return colNullable; } /** Only for testing. Normal client code should not need to call this. */ public JSpinner getColPrec() { return colPrec; } /** Only for testing. Normal client code should not need to call this. */ public JTextArea getColRemarks() { return colRemarks; } /** Only for testing. Normal client code should not need to call this. */ public JSpinner getColScale() { return colScale; } /** Only for testing. Normal client code should not need to call this. */ public JTree getColType() { return colType; } /** Only for testing. Normal client code should not need to call this. */ public JButton getSourceColumnButton() { return colSourceButton; } public boolean hasUnsavedChanges() { // TODO return whether this panel has been changed return true; } /** * The one instance of {@link CheckboxEnabler} that handles events from all * non-text components in this panel. */ private final CheckboxEnabler checkboxEnabler = new CheckboxEnabler(); /** * A simple listener that enables the checkbox associated with a component * whenever that component is manipulated by the user. */ private class CheckboxEnabler implements ActionListener, ChangeListener { public void actionPerformed(ActionEvent e) { enable((JComponent) e.getSource()); } public void stateChanged(ChangeEvent e) { enable((JComponent) e.getSource()); } private void enable(JComponent c) { JCheckBox checkBox = componentEnabledMap.get(c); if (checkBox != null) { checkBox.setSelected(true); } } } /** * Simple listener that enables the checkbox associated with a single * text component whenever its document changes. Instances of this listener * can't be shared among components; you need one instance per component. */ private class DocumentCheckboxEnabler implements DocumentListener { private final JCheckBox checkBox; public DocumentCheckboxEnabler(JCheckBox checkBox) { this.checkBox = checkBox; } public void changedUpdate(DocumentEvent e) { checkBox.setSelected(true); } public void insertUpdate(DocumentEvent e) { checkBox.setSelected(true); } public void removeUpdate(DocumentEvent e) { checkBox.setSelected(true); } } /** * Listens for SQLObject removals in the model that would make this * column editor obsolete (because it refers to properties of a * column that is no longer in the model). When this editor is deemed * obsolete, it looks for its nearest Window ancestor and disposes it. */ private final SPListener obsolesenceListener = new AbstractPoolingSPListener() { @Override public void childAddedImpl(SPChildEvent e) { logger.debug("SQLObject children got inserted: " + e); //$NON-NLS-1$ } /** * Checks to see if any of the columns being edited was just removed from * the playpen. If yes, disposes the enclosing window. */ @Override public void childRemovedImpl(SPChildEvent e) { logger.debug("SQLObject children got removed: " + e); //$NON-NLS-1$ for (SQLColumn column : columns) { if (e.getChild().equals(column) || e.getChild().equals(column.getParent())) { Window parentWindow = SwingUtilities.getWindowAncestor(panel); if (parentWindow != null) { parentWindow.dispose(); } } } } }; /** * Watches for this component becoming invisible and then unregisters it as a * listener on all the objects it has been listening to. */ private final AncestorListener cleanupListener = new AncestorListener() { public void ancestorAdded(AncestorEvent event) { /* don't care */ } public void ancestorMoved(AncestorEvent event) { /* don't care */ } public void ancestorRemoved(AncestorEvent event) { SQLPowerUtils.unlistenToHierarchy(session.getRootObject(), obsolesenceListener); } }; public void childAdded(SPChildEvent e) { //TODO Don't make the ColumnEditPanel a listener. //THEN make the PK check box go red if a conflicting update is received. } public void childRemoved(SPChildEvent e) { //TODO Don't make the ColumnEditPanel a listener. //THEN make the PK check box go red if a conflicting update is received. } public void propertyChanged(PropertyChangeEvent e) { String property = e.getPropertyName(); if (columns.contains(e.getSource())) { if (property.equals("name")) { DataEntryPanelChangeUtil.incomingChange(colLogicalName, e); } else if (property.equals("physicalName")) { DataEntryPanelChangeUtil.incomingChange(colPhysicalName, e); } else if (property.equals("type")) { DataEntryPanelChangeUtil.incomingChange(colType, e); } else if (property.equals("precision")) { DataEntryPanelChangeUtil.incomingChange(colPrec, e); } else if (property.equals("scale")) { DataEntryPanelChangeUtil.incomingChange(colScale, e); } else if (property.equals("inPK")) { DataEntryPanelChangeUtil.incomingChange(colInPK, e); } else if (property.equals("isNullable")) { DataEntryPanelChangeUtil.incomingChange(colNullable, e); } else if (property.equals("autoIncrement")) { DataEntryPanelChangeUtil.incomingChange(colAutoInc, e); } else if (property.equals("autoIncrementSequenceName")) { DataEntryPanelChangeUtil.incomingChange(colAutoIncSequenceName, e); } else if (property.equals("remarks")) { DataEntryPanelChangeUtil.incomingChange(colRemarks, e); } else if (property.equals("defaultValue")) { DataEntryPanelChangeUtil.incomingChange(colDefaultValue, e); } else { return; } setErrorText(DataEntryPanelChangeUtil.ERROR_MESSAGE); } else if (e.getSource() instanceof UserDefinedSQLType && columns.contains(((UserDefinedSQLType) e.getSource()).getParent())) { if (property.equals("type")) { DataEntryPanelChangeUtil.incomingChange(colType, e); } else { return; } setErrorText(DataEntryPanelChangeUtil.ERROR_MESSAGE); } } public void transactionEnded(TransactionEvent e) { //no-op } public void transactionRollback(TransactionEvent e) { //no-op } public void transactionStarted(TransactionEvent e) { //no-op } /** * If a user chooses a new Type to base the column on, then the UI * components for other properties like precision, scale, default value, * nullability, and autoincrement need to change to match that of the new * type. But the SQLColumn object itself must not change at that point, so * that it is simple to cancel any changes if the user chooses to click the * 'Cancel' button. * * @param sqlType * The data type to update all of the type fields to. * @param overrideIfNotNull * If true the override check boxes will be checked and the field * enabled if the value in the type given is not null. If false * the override checkboxes will never be checked to start and * just use the defaults given by the data type. */ private void updateSQLTypeComponents(UserDefinedSQLType sqlType, boolean overrideIfNotNull) { if (!componentEnabledMap.get(colType).isSelected()) { //not editing any of these fields, setting to defaults colScale.setValue(0); colPrec.setValue(0); colNullable.setSelectedItem(YesNoEnum.NO); colAutoInc.setSelectedItem(YesNoEnum.NO); colDefaultValue.setText(""); return; } if (sqlType.getScaleType(SQLTypePhysicalPropertiesProvider.GENERIC_PLATFORM) != PropertyType.VARIABLE) { typeOverrideMap.get(colScale).setSelected(false); typeOverrideMap.get(colScale).setEnabled(false); } else if (sqlType.getDefaultPhysicalProperties().getScale() == null || !overrideIfNotNull) { typeOverrideMap.get(colScale).setSelected(false); typeOverrideMap.get(colScale).setEnabled(true); } else { typeOverrideMap.get(colScale).setSelected(true); typeOverrideMap.get(colScale).setEnabled(true); } colScale.setValue(sqlType.getScale(SQLTypePhysicalPropertiesProvider.GENERIC_PLATFORM)); if (sqlType.getPrecisionType(SQLTypePhysicalPropertiesProvider.GENERIC_PLATFORM) != PropertyType.VARIABLE) { typeOverrideMap.get(colPrec).setSelected(false); typeOverrideMap.get(colPrec).setEnabled(false); } else if (sqlType.getDefaultPhysicalProperties().getPrecision() == null || !overrideIfNotNull) { typeOverrideMap.get(colPrec).setSelected(false); typeOverrideMap.get(colPrec).setEnabled(true); } else { typeOverrideMap.get(colPrec).setSelected(true); typeOverrideMap.get(colPrec).setEnabled(true); } colPrec.setValue(sqlType.getPrecision(SQLTypePhysicalPropertiesProvider.GENERIC_PLATFORM)); if (sqlType.getMyNullability() == null || !overrideIfNotNull) { typeOverrideMap.get(colNullable).setSelected(false); } else { typeOverrideMap.get(colNullable).setSelected(true); } colNullable.setSelectedItem(YesNoEnum.valueOf(sqlType.getNullability() == DatabaseMetaData.columnNullable)); if (sqlType.getDefaultPhysicalProperties().getDefaultValue() == null || !overrideIfNotNull) { typeOverrideMap.get(colDefaultValue).setSelected(false); } else { typeOverrideMap.get(colDefaultValue).setSelected(true); } colDefaultValue.setText(sqlType.getDefaultValue(SQLTypePhysicalPropertiesProvider.GENERIC_PLATFORM)); if (sqlType.getMyAutoIncrement() == null || !overrideIfNotNull) { typeOverrideMap.get(colAutoInc).setSelected(false); } else { typeOverrideMap.get(colAutoInc).setSelected(true); } colAutoInc.setSelectedItem(YesNoEnum.valueOf(sqlType.getAutoIncrement())); } /** * For testing purposes only. */ Map<JComponent, JCheckBox> getTypeOverrideMap() { return typeOverrideMap; } }