package fr.adrienbrault.idea.symfony2plugin.dic.container.util;
import com.intellij.openapi.project.Project;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.*;
import com.intellij.util.Consumer;
import com.intellij.util.indexing.FileBasedIndex;
import com.jetbrains.php.PhpIndex;
import com.jetbrains.php.lang.psi.elements.Field;
import com.jetbrains.php.lang.psi.elements.Method;
import com.jetbrains.php.lang.psi.elements.PhpClass;
import fr.adrienbrault.idea.symfony2plugin.config.yaml.YamlAnnotator;
import fr.adrienbrault.idea.symfony2plugin.dic.attribute.value.AttributeValueInterface;
import fr.adrienbrault.idea.symfony2plugin.dic.attribute.value.XmlTagAttributeValue;
import fr.adrienbrault.idea.symfony2plugin.dic.attribute.value.YamlKeyValueAttributeValue;
import fr.adrienbrault.idea.symfony2plugin.dic.container.SerializableService;
import fr.adrienbrault.idea.symfony2plugin.dic.container.ServiceSerializable;
import fr.adrienbrault.idea.symfony2plugin.dic.container.dict.ServiceTypeHint;
import fr.adrienbrault.idea.symfony2plugin.dic.container.visitor.ServiceConsumer;
import fr.adrienbrault.idea.symfony2plugin.stubs.ContainerCollectionResolver;
import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.ContainerIdUsagesStubIndex;
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils;
import fr.adrienbrault.idea.symfony2plugin.util.dict.ServiceUtil;
import fr.adrienbrault.idea.symfony2plugin.util.psi.PsiElementAssertUtil;
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.psi.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
/**
* @author Daniel Espendiller <daniel@espendiller.net>
*/
public class ServiceContainerUtil {
private static String[] LOWER_PRIORITY = new String[] {
"debug", "default", "abstract", "inner", "chain", "decorate", "delegat"
};
@NotNull
public static Collection<ServiceSerializable> getServicesInFile(@NotNull PsiFile psiFile) {
final Collection<ServiceSerializable> services = new ArrayList<>();
if(psiFile instanceof XmlFile) {
visitFile((XmlFile) psiFile, serviceConsumer -> {
SerializableService serializableService = createService(serviceConsumer);
serializableService.setDecorationInnerName(serviceConsumer.attributes().getString("decoration-inner-name"));
serializableService.setIsDeprecated(serviceConsumer.attributes().getBoolean("deprecated"));
services.add(serializableService);
});
} else if (psiFile instanceof YAMLFile) {
visitFile((YAMLFile) psiFile, serviceConsumer -> {
// alias inline "foo: @bar"
PsiElement yamlKeyValue = serviceConsumer.attributes().getPsiElement();
if(yamlKeyValue instanceof YAMLKeyValue) {
PsiElement value = ((YAMLKeyValue) yamlKeyValue).getValue();
if(value instanceof YAMLScalar) {
String valueText = ((YAMLScalar) value).getTextValue();
if(StringUtils.isNotBlank(valueText) && valueText.startsWith("@")) {
services.add(new SerializableService(serviceConsumer.getServiceId()).setAlias(valueText.substring(1)));
return;
}
}
}
SerializableService serializableService = createService(serviceConsumer);
serializableService.setDecorationInnerName(serviceConsumer.attributes().getString("decoration_inner_name"));
// catch: deprecated: ~
String string = serviceConsumer.attributes().getString("deprecated");
if("~".equals(string)) {
serializableService.setIsDeprecated(true);
} else {
serializableService.setIsDeprecated(serviceConsumer.attributes().getBoolean("deprecated"));
}
services.add(serializableService);
});
}
// decorated services
services.addAll(getPseudoDecoratedServices(services));
return services;
}
/**
* "espend.my_next_foo" > "espend.my_next_foo.inner" or custom inner name
*/
@NotNull
private static Collection<ServiceSerializable> getPseudoDecoratedServices(@NotNull Collection<ServiceSerializable> services) {
Collection<ServiceSerializable> decoratedServices = new ArrayList<>();
for (ServiceSerializable service : services) {
String decorates = service.getDecorates();
if(decorates == null || StringUtils.isBlank(decorates)) {
continue;
}
String decorationInnerName = service.getDecorationInnerName();
if(StringUtils.isBlank(decorationInnerName)) {
decorationInnerName = service.getId() + ".inner";
}
decoratedServices.add(new SerializableService(decorationInnerName));
}
return decoratedServices;
}
@NotNull
private static SerializableService createService(@NotNull ServiceConsumer serviceConsumer) {
AttributeValueInterface attributes = serviceConsumer.attributes();
Boolean anAbstract = attributes.getBoolean("abstract");
String aClass = StringUtils.stripStart(attributes.getString("class"), "\\");
if(aClass == null && isServiceIdAsClassSupported(attributes, anAbstract)) {
// if no "class" given since Syfmony 3.3 we have lowercase "id" names
// as we internally use case insensitive maps; add user provided values
aClass = serviceConsumer.getServiceId();
}
return new SerializableService(serviceConsumer.getServiceId())
.setAlias(attributes.getString("alias"))
.setClassName(aClass)
.setDecorates(attributes.getString("decorates"))
.setParent(attributes.getString("parent"))
.setIsAbstract(anAbstract)
.setIsAutowire(attributes.getBoolean("autowrite"))
.setIsLazy(attributes.getBoolean("lazy"))
.setIsPublic(attributes.getBoolean("public"));
}
/**
* Service definition allows "id" to "class" transformation: eg not an alias or abstract service
*/
private static boolean isServiceIdAsClassSupported(@NotNull AttributeValueInterface attributes, @Nullable Boolean anAbstract) {
return attributes.getString("alias") == null && !(anAbstract != null && anAbstract);
}
public static void visitFile(@NotNull YAMLFile psiFile, @NotNull Consumer<ServiceConsumer> consumer) {
for (YAMLKeyValue keyValue : YamlHelper.getQualifiedKeyValuesInFile(psiFile, "services")) {
String serviceId = keyValue.getKeyText();
if(StringUtils.isBlank(serviceId)) {
continue;
}
consumer.consume(new ServiceConsumer(keyValue, serviceId, new YamlKeyValueAttributeValue(keyValue)));
}
}
public static void visitFile(@NotNull XmlFile psiFile, @NotNull Consumer<ServiceConsumer> consumer) {
if(!(psiFile.getFirstChild() instanceof XmlDocument)) {
return;
}
XmlTag xmlTags[] = PsiTreeUtil.getChildrenOfType(psiFile.getFirstChild(), XmlTag.class);
if(xmlTags == null) {
return;
}
for(XmlTag xmlTag: xmlTags) {
if(xmlTag.getName().equals("container")) {
for(XmlTag servicesTag: xmlTag.getSubTags()) {
if(servicesTag.getName().equals("services")) {
for(XmlTag serviceTag: servicesTag.getSubTags()) {
String serviceId = serviceTag.getAttributeValue("id");
if(StringUtils.isBlank(serviceId)) {
continue;
}
consumer.consume(new ServiceConsumer(serviceTag, serviceId, new XmlTagAttributeValue(serviceTag)));
}
}
}
}
}
}
/**
* foo:
* class: Foo
* arguments: [@<caret>]
* arguments:
* - @<caret>
*/
@Nullable
public static ServiceTypeHint getYamlConstructorTypeHint(@NotNull PsiElement psiElement, @NotNull ContainerCollectionResolver.LazyServiceCollector lazyServiceCollector) {
if (!YamlAnnotator.isStringValue(psiElement)) {
return null;
}
// @TODO: simplify code checks
PsiElement yamlScalar = psiElement.getContext();
if(!(yamlScalar instanceof YAMLScalar)) {
return null;
}
return getYamlConstructorTypeHint((YAMLScalar) yamlScalar, lazyServiceCollector);
}
/**
* foo:
* class: Foo
* arguments: [@<caret>]
* arguments:
* - @<caret>
*/
@Nullable
public static ServiceTypeHint getYamlConstructorTypeHint(@NotNull YAMLScalar yamlScalar, @NotNull ContainerCollectionResolver.LazyServiceCollector lazyServiceCollector) {
PsiElement context = yamlScalar.getContext();
if(!(context instanceof YAMLSequenceItem)) {
return null;
}
final YAMLSequenceItem sequenceItem = (YAMLSequenceItem) context;
if (!(sequenceItem.getContext() instanceof YAMLSequence)) {
return null;
}
final YAMLSequence yamlArray = (YAMLSequence) sequenceItem.getContext();
if(!(yamlArray.getContext() instanceof YAMLKeyValue)) {
return null;
}
final YAMLKeyValue yamlKeyValue = (YAMLKeyValue) yamlArray.getContext();
if(!yamlKeyValue.getKeyText().equals("arguments")) {
return null;
}
YAMLMapping parentMapping = yamlKeyValue.getParentMapping();
if(parentMapping == null) {
return null;
}
final YAMLKeyValue classKeyValue = parentMapping.getKeyValueByKey("class");
if(classKeyValue == null) {
return null;
}
PhpClass serviceClass = ServiceUtil.getResolvedClassDefinition(yamlScalar.getProject(), classKeyValue.getValueText(), lazyServiceCollector);
if(serviceClass == null) {
return null;
}
Method constructor = serviceClass.getConstructor();
if(constructor == null) {
return null;
}
return new ServiceTypeHint(
constructor,
PsiElementUtils.getPrevSiblingsOfType(sequenceItem, PlatformPatterns.psiElement(YAMLSequenceItem.class)).size(),
yamlScalar
);
}
/**
* <services>
* <service class="Foo\\Bar\\Car">
* <argument type="service" id="<caret>" />
* </service>
* </services>
*/
@Nullable
public static ServiceTypeHint getXmlConstructorTypeHint(@NotNull PsiElement psiElement, @NotNull ContainerCollectionResolver.LazyServiceCollector lazyServiceCollector) {
if(!(psiElement.getContainingFile() instanceof XmlFile) || psiElement.getNode().getElementType() != XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN) {
return null;
}
XmlAttributeValue xmlAttributeValue = PsiTreeUtil.getParentOfType(psiElement, XmlAttributeValue.class);
if(xmlAttributeValue == null) {
return null;
}
XmlTag argumentTag = PsiTreeUtil.getParentOfType(psiElement, XmlTag.class);
if(argumentTag == null) {
return null;
}
XmlTag serviceTag = PsiElementAssertUtil.getParentOfTypeOrNull(argumentTag, XmlTag.class);
if(serviceTag == null) {
return null;
}
if(!serviceTag.getName().equals("service")) {
return null;
}
// service/argument[id]
String serviceDefName = serviceTag.getAttributeValue("class");
if(serviceDefName != null) {
PhpClass phpClass = ServiceUtil.getResolvedClassDefinition(psiElement.getProject(), serviceDefName);
// check type hint on constructor
if(phpClass != null) {
Method constructor = phpClass.getConstructor();
if(constructor != null) {
return new ServiceTypeHint(constructor, getArgumentIndex(argumentTag), psiElement);
}
}
}
return null;
}
/**
* <services>
* <service class="Foo\\Bar\\Car">
* <call method="foo"></call>
* <argument type="service" id="<caret>" />
* </call>
* </service>
* </services>
*/
@Nullable
public static ServiceTypeHint getXmlCallTypeHint(@NotNull PsiElement psiElement, @NotNull ContainerCollectionResolver.LazyServiceCollector lazyServiceCollector) {
// search for parent service definition
XmlTag currentXmlTag = PsiTreeUtil.getParentOfType(psiElement, XmlTag.class);
XmlTag parentXmlTag = PsiTreeUtil.getParentOfType(currentXmlTag, XmlTag.class);
if(parentXmlTag == null) {
return null;
}
String name = parentXmlTag.getName();
if(!"call".equals(name)) {
return null;
}
// service/call/argument[id]
XmlAttribute methodAttribute = parentXmlTag.getAttribute("method");
if(methodAttribute != null) {
String methodName = methodAttribute.getValue();
XmlTag serviceTag = parentXmlTag.getParentTag();
// get service class
if(serviceTag != null && "service".equals(serviceTag.getName())) {
XmlAttribute classAttribute = serviceTag.getAttribute("class");
if(classAttribute != null) {
String serviceDefName = classAttribute.getValue();
if(serviceDefName != null) {
PhpClass phpClass = ServiceUtil.getResolvedClassDefinition(psiElement.getProject(), serviceDefName);
// finally check method type hint
if(phpClass != null) {
Method method = phpClass.findMethodByName(methodName);
if(method != null) {
return new ServiceTypeHint(method, getArgumentIndex(currentXmlTag), psiElement);
}
}
}
}
}
}
return null;
}
/**
* Foobar::CONST
*/
@NotNull
public static Collection<PsiElement> getTargetsForConstant(@NotNull Project project, @NotNull String contents) {
// FOO
if (!contents.contains(":")) {
if(!contents.startsWith("\\")) {
contents = "\\" + contents;
}
return new ArrayList<>(
PhpIndex.getInstance(project).getConstantsByFQN(contents)
);
}
contents = contents.replaceAll(":+", ":");
String[] split = contents.split(":");
Collection<PsiElement> psiElements = new ArrayList<>();
for (PhpClass phpClass : PhpElementsUtil.getClassesInterface(project, split[0])) {
Field fieldByName = phpClass.findFieldByName(split[1], true);
if(fieldByName != null && fieldByName.isConstant()) {
psiElements.add(fieldByName);
}
}
return psiElements;
}
/**
* Calculate usage as of given service id in project scope
*/
public static int getServiceUsage(@NotNull Project project, @NotNull String id) {
int usage = 0;
List<Integer> values = FileBasedIndex.getInstance().getValues(ContainerIdUsagesStubIndex.KEY, id, GlobalSearchScope.allScope(project));
for (Integer integer : values) {
usage += integer;
}
return usage;
}
private static int getArgumentIndex(@NotNull XmlTag xmlTag) {
PsiElement psiElement = xmlTag;
int index = 0;
while (psiElement != null) {
psiElement = psiElement.getPrevSibling();
if(psiElement instanceof XmlTag && "argument".equalsIgnoreCase(((XmlTag) psiElement).getName())) {
index++;
}
}
return index;
}
public static boolean isLowerPriority(String name) {
for(String lowerName: LOWER_PRIORITY) {
if(name.contains(lowerName)) {
return true;
}
}
return false;
}
public static class ContainerServiceIdPriorityNameComparator implements Comparator<String> {
@Override
public int compare(String o1, String o2) {
if(isLowerPriority(o1) && isLowerPriority(o2)) {
return 0;
}
return isLowerPriority(o1) ? 1 : -1;
}
}
@NotNull
public static List<String> getSortedServiceId(@NotNull Project project, @NotNull Collection<String> ids) {
if(ids.size() == 0) {
return new ArrayList<>(ids);
}
List<String> myIds = new ArrayList<>(ids);
myIds.sort(new ServiceContainerUtil.ContainerServiceIdPriorityNameComparator());
myIds.sort((o1, o2) ->
((Integer) ServiceContainerUtil.getServiceUsage(project, o2))
.compareTo(ServiceContainerUtil.getServiceUsage(project, o1))
);
return myIds;
}
}