package fr.adrienbrault.idea.symfony2plugin.action; import com.intellij.ide.IdeView; import com.intellij.ide.highlighter.XmlFileType; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.DataContext; import com.intellij.openapi.actionSystem.LangDataKeys; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.fileTypes.FileType; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.util.io.StreamUtil; import com.intellij.psi.*; import com.intellij.psi.codeStyle.CodeStyleManager; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.xml.XmlAttribute; import com.intellij.psi.xml.XmlTag; import com.jetbrains.php.lang.psi.elements.Method; import com.jetbrains.php.lang.psi.elements.Parameter; import com.jetbrains.php.lang.psi.elements.PhpClass; import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons; import fr.adrienbrault.idea.symfony2plugin.action.ui.ServiceArgumentSelectionDialog; import fr.adrienbrault.idea.symfony2plugin.action.ui.SymfonyCreateService; 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.util.PhpElementsUtil; import fr.adrienbrault.idea.symfony2plugin.util.SymfonyBundleUtil; import fr.adrienbrault.idea.symfony2plugin.util.dict.ServiceUtil; import fr.adrienbrault.idea.symfony2plugin.util.dict.SymfonyBundle; import fr.adrienbrault.idea.symfony2plugin.util.yaml.YamlHelper; import org.apache.commons.lang.StringUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.yaml.YAMLFileType; import org.jetbrains.yaml.psi.*; import java.io.IOException; import java.util.*; import java.util.stream.Collectors; /** * @author Daniel Espendiller <daniel@espendiller.net> */ public class ServiceActionUtil { /** * Attributes which we should not support in missing arguments constructors for server definition */ public static final String[] INVALID_ARGUMENT_ATTRIBUTES = new String[]{ "parent", "factory-class", "factory-service", "abstract", "autowire" }; public static void buildFile(AnActionEvent event, final Project project, String templatePath) { String extension = templatePath.endsWith(".yml") ? "yml" : "xml" ; String fileName = Messages.showInputDialog(project, "File name (without extension)", String.format("Create %s Service", extension), Symfony2Icons.SYMFONY); if(fileName == null || StringUtils.isBlank(fileName)) { return; } FileType fileType = templatePath.endsWith(".yml") ? YAMLFileType.YML : XmlFileType.INSTANCE ; if(!fileName.endsWith("." + extension)) { fileName = fileName.concat("." + extension); } DataContext dataContext = event.getDataContext(); IdeView view = LangDataKeys.IDE_VIEW.getData(dataContext); if (view == null) { return; } PsiDirectory[] directories = view.getDirectories(); if(directories.length == 0) { return; } final PsiDirectory initialBaseDir = directories[0]; if (initialBaseDir == null) { return; } if(initialBaseDir.findFile(fileName) != null) { Messages.showInfoMessage("File exists", "Error"); return; } String content; try { content = StreamUtil.readText(ServiceActionUtil.class.getResourceAsStream(templatePath), "UTF-8").replace("\r\n", "\n"); } catch (IOException e) { e.printStackTrace(); return; } final PsiFileFactory factory = PsiFileFactory.getInstance(project); String bundleName = "Acme\\DemoBundle"; SymfonyBundleUtil symfonyBundleUtil = new SymfonyBundleUtil(project); SymfonyBundle symfonyBundle = symfonyBundleUtil.getContainingBundle(initialBaseDir); if(symfonyBundle != null) { bundleName = StringUtils.strip(symfonyBundle.getNamespaceName(), "\\"); } String underscoreBundle = bundleName.replace("\\", ".").toLowerCase(); if(underscoreBundle.endsWith("bundle")) { underscoreBundle = underscoreBundle.substring(0, underscoreBundle.length() - 6); } content = content.replace("{{ BundleName }}", bundleName).replace("{{ BundleNameUnderscore }}", underscoreBundle); final PsiFile file = factory.createFileFromText(fileName, fileType, content); ApplicationManager.getApplication().runWriteAction(() -> { CodeStyleManager.getInstance(project).reformat(file); initialBaseDir.add(file); }); PsiFile psiFile = initialBaseDir.findFile(fileName); if(psiFile != null) { view.selectElement(psiFile); } } @NotNull public static Set<String> getPossibleServices(@NotNull Project project, @NotNull String type, @NotNull Map<String, ContainerService> serviceClasses) { PhpClass typeClass = PhpElementsUtil.getClassInterface(project, type); if(typeClass == null) { return Collections.emptySet(); } return getPossibleServices(typeClass, serviceClasses); } @NotNull public static Set<String> getPossibleServices(@NotNull PhpClass phpClass, @NotNull Map<String, ContainerService> serviceClasses) { List<ContainerService> matchedContainer = new ArrayList<>(ServiceUtil.getServiceSuggestionForPhpClass(phpClass, serviceClasses)); if(matchedContainer.size() == 0) { return Collections.emptySet(); } // weak service have lower priority matchedContainer.sort(new SymfonyCreateService.ContainerServicePriorityWeakComparator()); // lower priority of services like "doctrine.orm.default_entity_manager" matchedContainer.sort(new SymfonyCreateService.ContainerServicePriorityNameComparator()); matchedContainer.sort((o1, o2) -> ((Integer)ServiceContainerUtil.getServiceUsage(phpClass.getProject(), o2.getName())) .compareTo(ServiceContainerUtil.getServiceUsage(phpClass.getProject(), o1.getName())) ); return matchedContainer.stream() .map(ContainerService::getName) .collect(Collectors.toCollection(LinkedHashSet::new)); } @NotNull public static Collection<XmlTag> getXmlContainerServiceDefinition(PsiFile psiFile) { Collection<XmlTag> xmlTags = new ArrayList<>(); for(XmlTag xmlTag: PsiTreeUtil.getChildrenOfTypeAsList(psiFile.getFirstChild(), XmlTag.class)) { if(xmlTag.getName().equals("container")) { for(XmlTag servicesTag: xmlTag.getSubTags()) { if(servicesTag.getName().equals("services")) { for(XmlTag parameterTag: servicesTag.getSubTags()) { if(parameterTag.getName().equals("service")) { xmlTags.add(parameterTag); } } } } } } return xmlTags; } public static class ServiceYamlContainer { @NotNull private final YAMLKeyValue serviceKey; @Nullable private final YAMLKeyValue argument; @NotNull private final String className; public ServiceYamlContainer(@NotNull YAMLKeyValue serviceKey, @Nullable YAMLKeyValue argument, @NotNull String className) { this.serviceKey = serviceKey; this.argument = argument; this.className = className; } @Nullable public YAMLKeyValue getArgument() { return argument; } @NotNull public String getClassName() { return className; } @NotNull public YAMLKeyValue getServiceKey() { return serviceKey; } /** * fo<caret>o: * class: foo * arguments: [] */ @Nullable public static ServiceYamlContainer create(@NotNull YAMLKeyValue yamlServiceKeyValue) { YAMLMapping childOfType = PsiTreeUtil.getChildOfType(yamlServiceKeyValue, YAMLMapping.class); if(childOfType == null) { return null; } YAMLKeyValue aClass = childOfType.getKeyValueByKey("class"); if(aClass == null) { return null; } YAMLValue value = aClass.getValue(); if(!(value instanceof YAMLScalar)) { return null; } String serviceClass = ((YAMLScalar) value).getTextValue(); if (StringUtils.isBlank(serviceClass)) { return null; } return new ServiceYamlContainer(yamlServiceKeyValue, childOfType.getKeyValueByKey("arguments"), serviceClass); } } /** * Gets all services inside yaml file with "arguments" key context */ @NotNull public static Collection<ServiceYamlContainer> getYamlContainerServiceArguments(@NotNull YAMLFile yamlFile) { Collection<ServiceYamlContainer> services = new ArrayList<>(); for(YAMLKeyValue yamlKeyValue : YamlHelper.getQualifiedKeyValuesInFile(yamlFile, "services")) { ServiceYamlContainer serviceYamlContainer = ServiceYamlContainer.create(yamlKeyValue); if(serviceYamlContainer != null) { services.add(serviceYamlContainer); } } return services; } @Nullable public static List<String> getXmlMissingArgumentTypes(@NotNull XmlTag xmlTag, boolean collectOptionalParameter, @NotNull ContainerCollectionResolver.LazyServiceCollector collector) { PhpClass resolvedClassDefinition = getPhpClassFromXmlTag(xmlTag, collector); if (resolvedClassDefinition == null) { return null; } Method constructor = resolvedClassDefinition.getConstructor(); if(constructor == null) { return null; } int serviceArguments = 0; for (XmlTag tag : xmlTag.getSubTags()) { if("argument".equals(tag.getName())) { serviceArguments++; } } Parameter[] parameters = collectOptionalParameter ? constructor.getParameters() : PhpElementsUtil.getFunctionRequiredParameter(constructor); if(parameters.length <= serviceArguments) { return null; } final List<String> args = new ArrayList<>(); for (int i = serviceArguments; i < parameters.length; i++) { Parameter parameter = parameters[i]; String s = parameter.getDeclaredType().toString(); args.add(s); } return args; } @Nullable public static PhpClass getPhpClassFromXmlTag(@NotNull XmlTag xmlTag, @NotNull ContainerCollectionResolver.LazyServiceCollector collector) { XmlAttribute classAttribute = xmlTag.getAttribute("class"); if(classAttribute == null) { return null; } String value = classAttribute.getValue(); if(StringUtils.isBlank(value)) { return null; } // @TODO: cache defs PhpClass resolvedClassDefinition = ServiceUtil.getResolvedClassDefinition(xmlTag.getProject(), value, collector); if(resolvedClassDefinition == null) { return null; } return resolvedClassDefinition; } @Nullable public static List<String> getYamlMissingArgumentTypes(Project project, ServiceActionUtil.ServiceYamlContainer container, boolean collectOptionalParameter, @NotNull ContainerCollectionResolver.LazyServiceCollector collector) { PhpClass resolvedClassDefinition = ServiceUtil.getResolvedClassDefinition(project, container.getClassName(), collector); if(resolvedClassDefinition == null) { return null; } Method constructor = resolvedClassDefinition.getConstructor(); if(constructor == null) { return null; } int serviceArguments = -1; if(container.getArgument() != null) { PsiElement yamlCompoundValue = container.getArgument().getValue(); if(yamlCompoundValue instanceof YAMLCompoundValue) { List<PsiElement> yamlArrayOnSequenceOrArrayElements = YamlHelper.getYamlArrayOnSequenceOrArrayElements((YAMLCompoundValue) yamlCompoundValue); if(yamlArrayOnSequenceOrArrayElements != null) { serviceArguments = yamlArrayOnSequenceOrArrayElements.size(); } } } else { serviceArguments = 0; } if(serviceArguments == -1) { return null; } Parameter[] parameters = collectOptionalParameter ? constructor.getParameters() : PhpElementsUtil.getFunctionRequiredParameter(constructor); if(parameters.length <= serviceArguments) { return null; } final List<String> args = new ArrayList<>(); for (int i = serviceArguments; i < parameters.length; i++) { Parameter parameter = parameters[i]; String s = parameter.getDeclaredType().toString(); args.add(s); } return args; } public static boolean isValidXmlParameterInspectionService(@NotNull XmlTag xmlTag) { // we dont support some attributes right now for(String s : INVALID_ARGUMENT_ATTRIBUTES) { if(xmlTag.getAttribute(s) != null) { return false; } } // <service><factory/></service> // symfony2 >= 2.6 for (XmlTag tag : xmlTag.getSubTags()) { if("factory".equals(tag.getName())) { return false; } } return true; } public static void fixServiceArgument(@NotNull List<String> args, final @NotNull XmlTag xmlTag) { fixServiceArgument(xmlTag.getProject(), args, new XmlInsertServicesCallback(xmlTag)); } public static void fixServiceArgument(@NotNull Project project, @NotNull List<String> args, final @NotNull InsertServicesCallback callback) { Map<String, ContainerService> services = ContainerCollectionResolver.getServices(project); Map<String, Set<String>> resolved = new LinkedHashMap<>(); for (String arg : args) { resolved.put(arg, ServiceActionUtil.getPossibleServices(project, arg, services)); } // we got an unique service list, not need to provide ui if(isUniqueServiceMap(resolved)) { List<String> items = new ArrayList<>(); for (Map.Entry<String, Set<String>> stringSetEntry : resolved.entrySet()) { Set<String> value = stringSetEntry.getValue(); if(value.size() > 0 ) { items.add(value.iterator().next()); } else { items.add("?"); } } callback.insert(items); return; } ServiceArgumentSelectionDialog.createDialog(project, resolved, callback::insert); } public static boolean isUniqueServiceMap(Map<String, Set<String>> resolvedServices) { for (Map.Entry<String, Set<String>> stringSetEntry : resolvedServices.entrySet()) { if(stringSetEntry.getValue().size() > 1) { return false; } } return true; } public static void addServices(List<String> items, XmlTag xmlTag) { for (String item : items) { if(StringUtils.isBlank(item)) { item = "?"; } XmlTag tag = XmlElementFactory.getInstance(xmlTag.getProject()).createTagFromText(String.format("<argument type=\"service\" id=\"%s\"/>", item), xmlTag.getLanguage()); xmlTag.addSubTag(tag, false); } } public interface InsertServicesCallback { void insert(List<String> items); } public static class XmlInsertServicesCallback implements InsertServicesCallback { @NotNull private final XmlTag xmlTag; public XmlInsertServicesCallback(final @NotNull XmlTag xmlTag) { this.xmlTag = xmlTag; } @Override public void insert(List<String> items) { addServices(items, this.xmlTag); } } }