package net.sourceforge.pmd.eclipse.ui.dialogs;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import net.sourceforge.pmd.PropertyDescriptor;
import net.sourceforge.pmd.PropertySource;
import net.sourceforge.pmd.Rule;
import net.sourceforge.pmd.eclipse.ui.nls.StringKeys;
import net.sourceforge.pmd.eclipse.ui.preferences.br.EditorFactory;
import net.sourceforge.pmd.eclipse.ui.preferences.br.RuleUIUtil;
import net.sourceforge.pmd.eclipse.ui.preferences.br.SizeChangeListener;
import net.sourceforge.pmd.eclipse.ui.preferences.br.ValueChangeListener;
import net.sourceforge.pmd.eclipse.ui.preferences.editors.MethodEditorFactory;
import net.sourceforge.pmd.eclipse.ui.preferences.editors.SWTUtil;
import net.sourceforge.pmd.eclipse.util.Util;
import net.sourceforge.pmd.lang.rule.XPathRule;
import net.sourceforge.pmd.util.StringUtil;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.TitleAreaDialog;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
/**
* Implements a dialog for adding or editing a rule property. As the user changes the specified
* type, each type's associated editor factory will provide additional labels & widgets intended
* to capture other metadata.
*
* @author Brian Remedios
*/
public class NewPropertyDialog extends TitleAreaDialog implements SizeChangeListener {
private Text nameField;
private Text labelField;
private Combo typeField;
private Control[] factoryControls;
private Composite dlgArea;
private EditorFactory factory;
private ValueChangeListener changeListener;
private PropertySource propertySource;
private PropertyDescriptor<?> descriptor;
private Map<Class<?>, EditorFactory> editorFactoriesByValueType;
// these are the ones we've tested, the others may work but might not make sense in the xpath source context...
private static final Class<?>[] validEditorTypes = new Class[] { String.class, Integer.class, Boolean.class, Class.class, Method.class };
private static final Class<?> defaultEditorType = validEditorTypes[0]; // first one
/**
* Constructor for RuleDialog. Supply a working descriptor with name & description values we expect the user to change.
*
* @param parentdlgArea
*/
public NewPropertyDialog(Shell parent, Map<Class<?>, EditorFactory> theEditorFactoriesByValueType, PropertySource theSource, ValueChangeListener theChangeListener) {
super(parent);
setShellStyle(SWT.DIALOG_TRIM | SWT.RESIZE | SWT.MAX | SWT.APPLICATION_MODAL);
propertySource = theSource;
changeListener = theChangeListener;
editorFactoriesByValueType = withOnly(theEditorFactoriesByValueType, validEditorTypes);
}
/**
* Constructor for RuleDialog.
*
* @param parentdlgArea
*/
public NewPropertyDialog(Shell parent, Map<Class<?>, EditorFactory> theEditorFactoriesByValueType, Rule theRule, PropertyDescriptor<?> theDescriptor, ValueChangeListener theChangeListener) {
this(parent, theEditorFactoriesByValueType, theRule, theChangeListener);
descriptor = theDescriptor;
}
public static Map<Class<?>, EditorFactory> withOnly(Map<Class<?>, EditorFactory> factoriesByType, Class<?>[] legalTypeKeys) {
Map<Class<?>, EditorFactory> results = new HashMap<Class<?>, EditorFactory>(legalTypeKeys.length);
for (Class<?> type : legalTypeKeys) {
if (factoriesByType.containsKey(type)) {
results.put(type, factoriesByType.get(type));
}
}
return results;
}
public boolean close() {
return super.close();
}
/**
* @see org.eclipse.jface.dialogs.Dialog#createDialogArea(Composite)
*/
@Override
protected Control createDialogArea(Composite parent) {
// parent.setLayoutData(new GridData(GridData.FILL_BOTH));
getShell().setText(SWTUtil.stringFor(StringKeys.DIALOG_PREFS_ADD_NEW_PROPERTY));
dlgArea = new Composite(parent, SWT.NULL);
GridLayout layout = new GridLayout(2, false);
layout.verticalSpacing = 2;
layout.marginTop = 1;
dlgArea.setLayout(layout);
dlgArea.setLayoutData(new GridData(GridData.FILL_BOTH));
buildLabel(dlgArea, "Name:"); nameField = buildNameText(dlgArea); // TODO i18l label
buildLabel(dlgArea, "Datatype:"); typeField = buildTypeField(dlgArea); // TODO i18l label
buildLabel(dlgArea, "Label:"); labelField = buildLabelField(dlgArea); // TODO i18l label
setPreferredName();
setInitialType();
dlgArea.pack();
return dlgArea;
}
protected Control createButtonBar(Composite parent) {
Control result = super.createButtonBar(parent);
validateForm();
return result;
}
/**
* Build a label
*/
private Label buildLabel(Composite parent, String msgKey) {
Label label = new Label(parent, SWT.NONE);
label.setText(msgKey == null ? "" : SWTUtil.stringFor(msgKey));
return label;
}
private void setFieldLayoutData(Control widget) {
GridData data = new GridData();
data.horizontalSpan = 1;
data.horizontalAlignment = GridData.FILL;
data.grabExcessHorizontalSpace = true;
widget.setLayoutData(data);
}
/**
* Build the rule name text
*/
private Text buildNameText(Composite parent) {
final Text text = new Text(parent, SWT.SINGLE | SWT.BORDER);
setFieldLayoutData(text);
text.addVerifyListener(RuleUIUtil.RuleNameVerifier);
text.addListener(SWT.Modify, new Listener() {
public void handleEvent(Event event) {
validateForm();
}
});
return text;
}
private boolean isValidNewLabel(String labelCandidate) {
return ! StringUtil.isEmpty(labelCandidate);
}
private boolean isPreExistingLabel(String labelCandidate) {
for (PropertyDescriptor<?> desc : propertySource.getPropertyDescriptors()) {
if (desc.description().equalsIgnoreCase(labelCandidate)) return false;
}
return true;
}
/**
* Build the rule name text
*/
private Text buildLabelField(Composite parent) {
final Text text = new Text(parent, SWT.SINGLE | SWT.BORDER);
setFieldLayoutData(text);
text.addVerifyListener(RuleUIUtil.RuleLabelVerifier);
text.addListener(SWT.Modify, new Listener() {
public void handleEvent(Event event) {
validateForm();
}
});
return text;
}
private static String labelFor(Class<?> type) {
return Util.signatureFor(type, MethodEditorFactory.UnwantedPrefixes);
}
/**
* A bit of a hack but this avoids the need to create an alternate lookup structure
*
* @param label
* @return
*/
private EditorFactory factoryFor(String label) {
for (Entry<Class<?>, EditorFactory> entry : editorFactoriesByValueType.entrySet()) {
if (label.equals(
labelFor(entry.getKey())
)) return entry.getValue();
}
return null;
}
/**
* Build the rule name text
*/
private Combo buildTypeField(final Composite parent) {
final Combo combo = new Combo(parent, SWT.READ_ONLY);
setFieldLayoutData(combo);
String[] labels = new String[editorFactoriesByValueType.size()];
int i=0;
for (Entry<Class<?>, EditorFactory> entry : editorFactoriesByValueType.entrySet()) {
labels[i++] = labelFor(entry.getKey());
}
Arrays.sort(labels);
combo.setItems(labels);
combo.addSelectionListener(new SelectionAdapter() {
public void widgetSelected(SelectionEvent e) {
int selectionIdx = combo.getSelectionIndex();
EditorFactory factory = factoryFor(combo.getItem(selectionIdx));
factory( factory );
}
});
return combo;
}
private boolean ruleHasPropertyName(String name) {
return propertySource.getPropertyDescriptor(name) != null;
}
/**
* Pick the first name in the xpath source the rule doesn't know about
*/
private void setPreferredName() {
String xpath = propertySource.getProperty(XPathRule.XPATH_DESCRIPTOR).trim();
List<int[]> positions = Util.referencedNamePositionsIn(xpath, '$');
List<String> names = Util.fragmentsWithin(xpath, positions);
for (String name : names) {
if (ruleHasPropertyName(name)) continue;
nameField.setText(name);
return;
}
nameField.setText("");
}
private void setInitialType() {
String editorLabel = labelFor(defaultEditorType);
typeField.select( Util.indexOf(typeField.getItems(), editorLabel) );
factory( factoryFor(editorLabel));
}
private void cleanFactoryStuff() {
if (factoryControls != null) for (Control control : factoryControls) control.dispose();
}
private void factory(EditorFactory theFactory) {
factory = theFactory;
descriptor = factory.createDescriptor("??", "??", null); // dummy values that will be replaced
labelField.setText(descriptor.description());
cleanFactoryStuff();
factoryControls = factory.createOtherControlsOn(dlgArea, descriptor, propertySource, changeListener, this);
dlgArea.getShell().layout();
dlgArea.pack();
dlgArea.getParent().pack();
}
// /**
// * Helper method to shorten message access
// *
// * @param key a message key
// * @return requested message
// */
// private String getMessage(String key) {
// return PMDPlugin.getDefault().getStringTable().getString(key);
// }
/**
* @see org.eclipse.jface.dialogs.Dialog#okPressed()
*/
@Override
protected void okPressed() {
if (validateForm() ) {
descriptor = newDescriptor();
super.okPressed();
}
}
/**
* Perform the form validation
*/
private boolean validateForm() {
boolean isOk = validateName() && validateLabel();
Control button = getButton(IDialogConstants.OK_ID);
if (button != null) button.setEnabled(isOk);
return isOk;
}
/**
* Perform the name validation
*/
private boolean validateName() {
String name = nameField.getText().trim();
if (StringUtil.isEmpty(name)) {
setErrorMessage(
"A property name is required"
);
return false;
}
if (ruleHasPropertyName(name)) {
setErrorMessage(
"'" + name + "' is already used by another property"
);
return false;
}
setErrorMessage(null);
return true;
}
/**
* Perform the label validation
*/
private boolean validateLabel() {
String label = labelField.getText().trim();
if (StringUtil.isEmpty(label)) {
setErrorMessage(
"A descriptive label is required"
);
return false;
}
if (!isValidNewLabel(label)) {
setErrorMessage("Invalid label");
return false;
}
if (!isPreExistingLabel(label)) {
setErrorMessage(
"Label text must differ from other label text"
);
return false;
}
setErrorMessage(null);
return true;
}
/**
* Returns the descriptor.
*
* @return PropertyDescriptor
*/
public PropertyDescriptor<?> descriptor() {
return descriptor;
}
private PropertyDescriptor<?> newDescriptor() {
return factory.createDescriptor(
nameField.getText().trim(),
labelField.getText().trim(),
factoryControls
);
}
/**
* @see org.eclipse.jface.dialogs.Dialog#cancelPressed()
*/
@Override
protected void cancelPressed() {
// courierFont.dispose();
super.cancelPressed();
}
public void addedRows(int newRowCount) {
dlgArea.pack();
dlgArea.getParent().pack();
System.out.println("rows added: " + newRowCount);
}
}