package fr.adrienbrault.idea.symfony2plugin.action.ui;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.ScrollType;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.ComboBox;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.XmlElementFactory;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import com.intellij.ui.ToolbarDecorator;
import com.intellij.ui.table.TableView;
import com.intellij.util.ui.ColumnInfo;
import com.intellij.util.ui.ListTableModel;
import com.jetbrains.php.lang.psi.elements.*;
import fr.adrienbrault.idea.symfony2plugin.Settings;
import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons;
import fr.adrienbrault.idea.symfony2plugin.action.ServiceActionUtil;
import fr.adrienbrault.idea.symfony2plugin.dic.ContainerService;
import fr.adrienbrault.idea.symfony2plugin.dic.container.util.ServiceContainerUtil;
import fr.adrienbrault.idea.symfony2plugin.stubs.ContainerCollectionResolver;
import fr.adrienbrault.idea.symfony2plugin.ui.utils.ClassCompletionPanelWrapper;
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
import fr.adrienbrault.idea.symfony2plugin.util.dict.ServiceUtil;
import fr.adrienbrault.idea.symfony2plugin.util.yaml.YamlHelper;
import fr.adrienbrault.idea.symfony2plugin.util.yaml.YamlPsiElementFactory;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.yaml.psi.YAMLFile;
import org.jetbrains.yaml.psi.YAMLKeyValue;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.table.TableCellEditor;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.KeyEvent;
import java.io.IOException;
import java.util.*;
import java.util.List;
/**
* @author Daniel Espendiller <daniel@espendiller.net>
*/
public class SymfonyCreateService extends JDialog {
private JPanel panel1;
private JPanel content;
private JPanel tableViewPanel;
private JTextArea textAreaOutput;
private JButton generateButton;
private JButton buttonCopy;
private JButton closeButton;
private JRadioButton radioButtonOutXml;
private JRadioButton radioButtonOutYaml;
private JTextField textFieldServiceName;
private JButton buttonSettings;
private JButton buttonInsert;
private JPanel panelFoo;
private TableView<MethodParameter.MethodModelParameter> tableView;
private ListTableModel<MethodParameter.MethodModelParameter> modelList;
private Map<String, ContainerService> serviceClass;
private Set<String> serviceSetComplete;
private Project project;
@Nullable
private PsiFile psiFile;
@Nullable
private Editor editor;
@Nullable
private String classInit;
private ClassCompletionPanelWrapper classCompletionPanelWrapper;
public SymfonyCreateService(@NotNull final Project project, @Nullable PsiFile psiFile, @Nullable Editor editor, @NotNull String className) {
this(project, psiFile, editor);
this.classInit = className;
}
public SymfonyCreateService(@NotNull final Project project, @Nullable PsiFile psiFile, @Nullable Editor editor) {
this.project = project;
this.psiFile = psiFile;
this.editor = editor;
}
public void init() {
setContentPane(panel1);
setModal(true);
this.classCompletionPanelWrapper = new ClassCompletionPanelWrapper(project, panelFoo, s -> update());
this.modelList = new ListTableModel<>(
new IconColumn(),
new NamespaceColumn(),
new ParameterIndexColumn(),
new ServiceColumn(),
new IsServiceColumn()
);
// set default output language on last user selection
String lastServiceGeneratorLanguage = Settings.getInstance(project).lastServiceGeneratorLanguage;
if ("xml".equalsIgnoreCase(lastServiceGeneratorLanguage)) {
radioButtonOutXml.setSelected(true);
} else if ("yaml".equalsIgnoreCase(lastServiceGeneratorLanguage)) {
radioButtonOutYaml.setSelected(true);
}
// overwrite language output on direct file context
if(this.psiFile instanceof YAMLFile) {
radioButtonOutYaml.setSelected(true);
} else if(this.psiFile instanceof XmlFile) {
radioButtonOutXml.setSelected(true);
}
// lets use yaml as default
if(!radioButtonOutYaml.isSelected() && !radioButtonOutXml.isSelected()) {
radioButtonOutYaml.setSelected(true);
}
this.tableView = new TableView<>();
this.tableView.setModelAndUpdateColumns(this.modelList);
tableViewPanel.add(ToolbarDecorator.createDecorator(this.tableView)
.disableAddAction()
.disableDownAction()
.disableRemoveAction()
.disableUpDownActions()
.createPanel()
);
this.serviceClass = ContainerCollectionResolver.getServices(project);
this.serviceSetComplete = new TreeSet<>();
serviceSetComplete.add("");
serviceSetComplete.addAll(this.serviceClass.keySet());
//update();
this.modelList.addTableModelListener(e -> generateServiceDefinition());
this.generateButton.addActionListener(e -> update());
this.closeButton.addActionListener(e -> {
setEnabled(false);
dispose();
});
this.buttonCopy.addActionListener(e -> {
if(StringUtils.isBlank(textAreaOutput.getText())) {
return;
}
StringSelection stringSelection = new StringSelection(textAreaOutput.getText());
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard ();
clipboard.setContents(stringSelection, null);
});
this.buttonSettings.addActionListener(e -> SymfonyJavascriptServiceNameForm.create(SymfonyCreateService.this, project, classCompletionPanelWrapper.getClassName()));
initClassName();
radioButtonOutXml.addChangeListener(e -> generateServiceDefinition());
radioButtonOutYaml.addChangeListener(e -> generateServiceDefinition());
textFieldServiceName.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
generateServiceDefinition();
}
@Override
public void removeUpdate(DocumentEvent e) {
generateServiceDefinition();
}
@Override
public void changedUpdate(DocumentEvent e) {
generateServiceDefinition();
}
});
// exit on "esc" key
this.getRootPane().registerKeyboardAction(e -> dispose(), KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW);
// insert
if(this.psiFile instanceof XmlFile || this.psiFile instanceof YAMLFile) {
this.buttonInsert.setEnabled(true);
this.buttonInsert.setVisible(true);
this.buttonInsert.requestFocusInWindow();
this.getRootPane().setDefaultButton(buttonInsert);
this.buttonInsert.addActionListener(e -> {
if(psiFile instanceof XmlFile) {
insertXmlServiceTag();
} else if(psiFile instanceof YAMLFile) {
insertYamlServiceTag();
}
});
} else {
this.buttonInsert.setEnabled(false);
this.buttonInsert.setVisible(false);
}
}
private void initClassName() {
if(this.classInit != null) {
classCompletionPanelWrapper.setClassName(StringUtils.stripStart(this.classInit, "\\"));
return;
}
try {
String data = (String) Toolkit.getDefaultToolkit().getSystemClipboard().getData(DataFlavor.stringFlavor);
if(data != null && data.length() <= 255 && data.matches("[_A-Za-z0-9\\\\]+")) {
classCompletionPanelWrapper.setClassName(data);
}
} catch (UnsupportedFlavorException | IOException ignored) {
}
}
private void insertYamlServiceTag() {
if(!(this.psiFile instanceof YAMLFile)) {
return;
}
String text = createServiceAsText(ServiceBuilder.OutputType.Yaml);
YAMLKeyValue fromText = YamlPsiElementFactory.createFromText(project, YAMLKeyValue.class, text);
if(fromText == null) {
return;
}
PsiElement psiElement = YamlHelper.insertKeyIntoFile((YAMLFile) psiFile, fromText, "services");
if(psiElement != null) {
navigateToElement(new TextRange[] {psiElement.getTextRange()});
}
dispose();
}
private void insertXmlServiceTag() {
final XmlTag rootTag = ((XmlFile) SymfonyCreateService.this.psiFile).getRootTag();
if(rootTag == null) {
return;
}
final TextRange[] textRange = {null};
new WriteCommandAction.Simple(project, "Generate Service", SymfonyCreateService.this.psiFile) {
@Override
protected void run() {
XmlTag services = rootTag.findFirstSubTag("services");
XmlElementFactory instance = XmlElementFactory.getInstance(SymfonyCreateService.this.project);
if(services == null) {
services = rootTag.addSubTag(instance.createTagFromText("<services/>", rootTag.getLanguage()), false);
}
XmlTag tag = instance.createTagFromText(createServiceAsText(ServiceBuilder.OutputType.XML).replace("\r\n", "\n").replace("\n", " "), services.getLanguage());
textRange[0] = services.addSubTag(tag, false).getTextRange();
}
}.execute();
navigateToElement(textRange);
dispose();
}
private void navigateToElement(TextRange[] textRange) {
if(editor != null && textRange[0] != null) {
editor.getCaretModel().moveToOffset(textRange[0].getStartOffset() + 1);
editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
}
}
private String createServiceAsText(@NotNull ServiceBuilder.OutputType outputType) {
return new ServiceBuilder(this.modelList.getItems(), this.project).build(
outputType,
StringUtils.stripStart(classCompletionPanelWrapper.getClassName(), "\\"),
textFieldServiceName.getText()
);
}
private void generateServiceDefinition() {
String className = classCompletionPanelWrapper.getClassName();
if(StringUtils.isBlank(className)) {
return;
}
if(className.startsWith("\\")) {
className = className.substring(1);
}
// after cleanup class is empty
if(StringUtils.isBlank(className)) {
return;
}
ServiceBuilder.OutputType outputType = ServiceBuilder.OutputType.XML;
if(radioButtonOutYaml.isSelected()) {
outputType = ServiceBuilder.OutputType.Yaml;
}
// save last selection
Settings.getInstance(project).lastServiceGeneratorLanguage = outputType.toString().toLowerCase();
textAreaOutput.setText(createServiceAsText(outputType));
}
private void update() {
ApplicationManager.getApplication().runReadAction(new Thread(this::updateTask));
}
private void updateTask() {
String className = classCompletionPanelWrapper.getClassName();
if(className.startsWith("\\")) {
className = className.substring(1);
}
if(className.length() == 0) {
return;
}
textFieldServiceName.setText("");
PhpClass phpClass = PhpElementsUtil.getClass(project, className);
if(phpClass == null) {
return;
}
textFieldServiceName.setText(ServiceUtil.getServiceNameForClass(project, className));
List<MethodParameter.MethodModelParameter> modelParameters = new ArrayList<>();
for(Method method: phpClass.getMethods()) {
if(method.getModifier().isPublic()) {
Parameter[] parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
Set<String> possibleServices = getPossibleServices(parameters[i]);
if(possibleServices.size() > 0) {
modelParameters.add(new MethodParameter.MethodModelParameter(method, parameters[i], i, possibleServices, getServiceName(possibleServices)));
} else {
modelParameters.add(new MethodParameter.MethodModelParameter(method, parameters[i], i, serviceSetComplete));
}
}
}
method.getName();
}
modelParameters.sort((o1, o2) -> {
int i = o1.getName().compareTo(o2.getName());
if (i != 0) {
return i;
}
return Integer.valueOf(o1.getIndex()).compareTo(o2.getIndex());
});
while(this.modelList.getRowCount() > 0) {
this.modelList.removeRow(0);
}
this.modelList.addRows(modelParameters);
generateServiceDefinition();
}
@Nullable
private String getServiceName(Set<String> services) {
if(services.size() == 0) {
return null;
}
// we have a weight sorted Set, so first one
return services.iterator().next();
}
private class IsServiceColumn extends ColumnInfo<MethodParameter.MethodModelParameter, Boolean> {
public IsServiceColumn() {
super("Act");
}
@Nullable
@Override
public Boolean valueOf(MethodParameter.MethodModelParameter modelParameter) {
return modelParameter.isPossibleService();
}
public void setValue(MethodParameter.MethodModelParameter modelParameter, Boolean value){
modelParameter.setPossibleService(value);
tableView.getListTableModel().fireTableDataChanged();
}
public Class getColumnClass() {
return Boolean.class;
}
public boolean isCellEditable(MethodParameter.MethodModelParameter modelParameter) {
return true;
}
@Override
public int getWidth(JTable table) {
return 36;
}
}
private class ServiceColumn extends ColumnInfo<MethodParameter.MethodModelParameter, String> {
public ServiceColumn() {
super("Service");
}
@Nullable
@Override
public String valueOf(MethodParameter.MethodModelParameter modelParameter) {
return modelParameter.getCurrentService();
}
public void setValue(MethodParameter.MethodModelParameter modelParameter, String value){
modelParameter.setCurrentService(value);
tableView.getListTableModel().fireTableDataChanged();
}
@Override
public boolean isCellEditable(MethodParameter.MethodModelParameter modelParameter) {
return true;
}
@Nullable
@Override
public TableCellEditor getEditor(MethodParameter.MethodModelParameter modelParameter) {
Set<String> sorted = modelParameter.getPossibleServices();
ComboBox comboBox = new ComboBox(sorted.toArray(new String[sorted.size()] ), 200);
comboBox.setEditable(true);
return new DefaultCellEditor(comboBox);
}
}
private class NamespaceColumn extends ColumnInfo<MethodParameter.MethodModelParameter, String> {
public NamespaceColumn() {
super("Method");
}
@Nullable
@Override
public String valueOf(MethodParameter.MethodModelParameter modelParameter) {
return modelParameter.getName();
}
}
private class IconColumn extends ColumnInfo<MethodParameter.MethodModelParameter, Icon> {
public IconColumn() {
super("");
}
@Nullable
@Override
public Icon valueOf(MethodParameter.MethodModelParameter modelParameter) {
return modelParameter.getMethod().getIcon();
}
public java.lang.Class getColumnClass() {
return ImageIcon.class;
}
@Override
public int getWidth(JTable table) {
return 32;
}
}
private class ParameterIndexColumn extends ColumnInfo<MethodParameter.MethodModelParameter, String> {
public ParameterIndexColumn() {
super("Parameter");
}
@Nullable
@Override
public String valueOf(MethodParameter.MethodModelParameter modelParameter) {
return modelParameter.getParameter().getName();
}
}
private Set<String> getPossibleServices(Parameter parameter) {
PhpPsiElement phpPsiElement = parameter.getFirstPsiChild();
if(!(phpPsiElement instanceof ClassReference)) {
return Collections.emptySet();
}
String type = ((ClassReference) phpPsiElement).getFQN();
if(type == null) {
return Collections.emptySet();
}
return ServiceActionUtil.getPossibleServices(project, type, serviceClass);
}
public static class ContainerServicePriorityNameComparator implements Comparator<ContainerService> {
@Override
public int compare(ContainerService o1, ContainerService o2) {
if(ServiceContainerUtil.isLowerPriority(o1.getName()) && ServiceContainerUtil.isLowerPriority(o2.getName())) {
return 0;
}
if(ServiceContainerUtil.isLowerPriority(o1.getName())) {
return 1;
}
return -1;
}
}
public static class ContainerServicePriorityWeakComparator implements Comparator<ContainerService> {
@Override
public int compare(ContainerService o1, ContainerService o2) {
if(o1.isWeak() == o2.isWeak()) {
return 0;
}
return (o1.isWeak() ? 1 : -1);
}
}
private static SymfonyCreateService prepare(@NotNull Component component, @NotNull SymfonyCreateService service) {
service.init();
service.setTitle("Symfony: Service Generator");
service.setIconImage(Symfony2Icons.getImage(Symfony2Icons.SYMFONY));
service.pack();
service.setMinimumSize(new Dimension(550, 250));
service.setLocationRelativeTo(component);
service.setVisible(true);
return service;
}
public static SymfonyCreateService create(@NotNull Component component, @NotNull Project project, @NotNull PsiFile psiFile, @Nullable Editor editor) {
return prepare(component, new SymfonyCreateService(project, psiFile, editor));
}
public static SymfonyCreateService create(@NotNull Component component, @NotNull Project project, @NotNull PsiFile psiFile, @NotNull PhpClass phpClass, @Nullable Editor editor) {
return prepare(component, new SymfonyCreateService(project, psiFile, editor, phpClass.getFQN()));
}
}