package fr.adrienbrault.idea.symfony2plugin.stubs;
import com.intellij.openapi.extensions.ExtensionPointName;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.CachedValue;
import com.intellij.util.indexing.FileBasedIndexImpl;
import fr.adrienbrault.idea.symfony2plugin.config.component.parser.ParameterServiceParser;
import fr.adrienbrault.idea.symfony2plugin.dic.ContainerParameter;
import fr.adrienbrault.idea.symfony2plugin.dic.ContainerService;
import fr.adrienbrault.idea.symfony2plugin.dic.XmlServiceParser;
import fr.adrienbrault.idea.symfony2plugin.dic.container.ServiceInterface;
import fr.adrienbrault.idea.symfony2plugin.dic.container.ServiceSerializable;
import fr.adrienbrault.idea.symfony2plugin.dic.container.dict.ContainerBuilderCall;
import fr.adrienbrault.idea.symfony2plugin.extension.ServiceCollectorParameter;
import fr.adrienbrault.idea.symfony2plugin.stubs.cache.FileIndexCaches;
import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.ContainerBuilderStubIndex;
import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.ContainerParameterStubIndex;
import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.ServicesDefinitionStubIndex;
import fr.adrienbrault.idea.symfony2plugin.util.service.ServiceXmlParserFactory;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
/**
* @author Daniel Espendiller <daniel@espendiller.net>
*/
public class ContainerCollectionResolver {
private static final Key<CachedValue<Map<String, List<ServiceSerializable>>>> SERVICE_CONTAINER_INDEX = new Key<>("SYMFONY_SERVICE_CONTAINER_INDEX");
private static final Key<CachedValue<Map<String, List<String>>>> SERVICE_PARAMETER_INDEX = new Key<>("SERVICE_PARAMETER_INDEX");
private static final Key<CachedValue<Set<String>>> SERVICE_CONTAINER_INDEX_NAMES = new Key<>("SYMFONY_SERVICE_CONTAINER_INDEX_NAMES");
private static final Key<CachedValue<Set<String>>> SERVICE_PARAMETER_INDEX_NAMES = new Key<>("SERVICE_PARAMETER_INDEX_NAMES");
private static final ExtensionPointName<fr.adrienbrault.idea.symfony2plugin.extension.ServiceCollector> EXTENSIONS = new ExtensionPointName<>(
"fr.adrienbrault.idea.symfony2plugin.extension.ServiceCollector"
);
public static Collection<String> getServiceNames(@NotNull Project project) {
return ServiceCollector.create(project).getNames();
}
public static boolean hasServiceNames(@NotNull Project project, @NotNull String serviceName) {
// @TODO: we dont need a collection here; stop on first match
return ServiceCollector.create(project).getNames().contains(serviceName);
}
@Nullable
public static ContainerService getService(@NotNull Project project, @NotNull String serviceName) {
Map<String, ContainerService> services = getServices(project);
return services.containsKey(serviceName) ? services.get(serviceName) : null;
}
public static Map<String, ContainerService> getServices(@NotNull Project project) {
return ServiceCollector.create(project).getServices();
}
@Nullable
public static String resolveService(@NotNull Project project, @NotNull String serviceName) {
return ServiceCollector.create(project).resolve(serviceName);
}
public static class LazyServiceCollector {
private final Project project;
private ServiceCollector serviceCollector;
private ParameterCollector parameterCollector;
public LazyServiceCollector(Project project) {
this.project = project;
}
@NotNull
public ServiceCollector getCollector() {
if(this.serviceCollector == null) {
this.serviceCollector = ServiceCollector.create(project);
}
return this.serviceCollector;
}
@NotNull
public ParameterCollector getParameterCollector() {
if(this.parameterCollector == null) {
this.parameterCollector = ParameterCollector.create(project);
}
return this.parameterCollector;
}
}
/**
*
* Resolve service class name which can be a class name or parameter, unknown parameter returns null
*
* @param project project
* @param paramOrClassName any raw class name or parameter name
* @return class name or unchanged item
*/
@Nullable
public static String resolveParameter(@NotNull Project project, @NotNull String paramOrClassName) {
return resolveParameter(ParameterCollector.create(project), paramOrClassName);
}
@NotNull
public static Map<String, ContainerParameter> getParameters(@NotNull Project project) {
return ParameterCollector.create(project).getParameters();
}
@Nullable
public static String resolveParameter(@NotNull ParameterCollector parameterCollector, @NotNull String paramOrClassName) {
return parameterCollector.resolve(paramOrClassName);
}
public static Set<String> getParameterNames(@NotNull Project project) {
return ParameterCollector.create(project).getNames();
}
public static class ServiceCollector {
@NotNull
final private Project project;
@Nullable
private ParameterCollector parameterCollector;
@Nullable
private Map<String, ContainerService> services;
public ServiceCollector(@NotNull Project project) {
this.project = project;
}
public Collection<ContainerService> collect() {
return this.getServices().values();
}
@Nullable
public String resolve(String serviceName) {
if(this.getServices().containsKey(serviceName)) {
// service can be a parameter, resolve if necessary
ContainerService service = this.getServices().get(serviceName);
String className = service.getClassName();
if(className != null && className.startsWith("%") && className.endsWith("%")) {
return getParameterCollector().resolve(className);
} else {
return className;
}
}
return null;
}
public Map<String, ContainerService> getServices() {
if(this.services != null) {
return this.services;
}
this.services = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
// file system
for(Map.Entry<String, String> entry: ServiceXmlParserFactory.getInstance(project, XmlServiceParser.class).getServiceMap().getMap().entrySet()) {
services.put(entry.getKey(), new ContainerService(entry.getKey(), entry.getValue()));
}
Collection<ServiceInterface> aliases = new ArrayList<>();
Collection<ServiceInterface> decorated = new ArrayList<>();
// Extension points
ServiceCollectorParameter.Service parameter = null;
Collection<ServiceInterface> exps = new ArrayList<>();
for (fr.adrienbrault.idea.symfony2plugin.extension.ServiceCollector collectorEx : EXTENSIONS.getExtensions()) {
if(parameter == null) {
parameter = new ServiceCollectorParameter.Service(project, exps);
}
collectorEx.collectServices(parameter);
}
if(exps.size() > 0) {
exps.forEach(service -> services.put(service.getId(), new ContainerService(service, null)));
}
for (Map.Entry<String, List<ServiceSerializable>> entry : FileIndexCaches.getSetDataCache(project, SERVICE_CONTAINER_INDEX, SERVICE_CONTAINER_INDEX_NAMES, ServicesDefinitionStubIndex.KEY, ServiceIndexUtil.getRestrictedFileTypesScope(project)).entrySet()) {
// dont work twice on service;
// @TODO: to need to optimize this to decorate as much service data as possible
String serviceName = entry.getKey();
// fake empty service, case which is not allowed by catch it
List<ServiceSerializable> services = entry.getValue();
if(services.size() == 0) {
this.services.put(serviceName, new ContainerService(serviceName, null, true));
continue;
}
for(ServiceInterface service: services) {
String classValue = service.getClassName();
// duplicate services
if(this.services.containsKey(serviceName)) {
if(classValue == null) {
continue;
}
String compiledClassName = this.services.get(serviceName).getClassName();
if(classValue.equalsIgnoreCase(compiledClassName)) {
continue;
}
String resolvedClassValue = getParameterCollector().resolve(classValue);
if(resolvedClassValue != null && !StringUtils.isBlank(classValue) && !resolvedClassValue.equalsIgnoreCase(compiledClassName)) {
this.services.get(serviceName).addClassName(resolvedClassValue);
}
continue;
}
if(service.getAlias() != null) {
aliases.add(service);
}
// reuse iteration for alias mapping
if(service.getDecorates() != null) {
decorated.add(service);
}
// resolve class value, it can be null or a parameter
if(!StringUtils.isBlank(classValue)) {
classValue = getParameterCollector().resolve(classValue);
}
// @TODO: legacy bridge; replace this with ServiceInterface
this.services.put(serviceName, new ContainerService(service, classValue));
}
}
// replace alias with main service
if(aliases.size() > 0) {
collectAliases(aliases);
}
if(decorated.size() > 0) {
collectDecorated(decorated);
}
return this.services;
}
private void collectAliases(@NotNull Collection<ServiceInterface> aliases) {
for (ServiceInterface service : aliases) {
// double check alias name
String alias = service.getAlias();
if(alias == null || StringUtils.isBlank(alias) || !this.services.containsKey(alias)) {
continue;
}
this.services.put(service.getId(), this.services.get(alias));
}
}
private void collectDecorated(@NotNull Collection<ServiceInterface> decorated) {
for (ServiceInterface service : decorated) {
String decorationInnerName = service.getDecorationInnerName();
if(StringUtils.isBlank(decorationInnerName)) {
decorationInnerName = service.getId() + ".inner";
}
ContainerService origin = this.services.get(service.getDecorates());
if(origin == null) {
continue;
}
// @TODO: migrate constructor to ServiceInterface and decorate
ContainerService value = new ContainerService(decorationInnerName, origin.getClassName(), origin.isWeak(), origin.isPrivate());
origin.getClassNames().forEach(value::addClassName);
this.services.put(decorationInnerName, value);
}
}
public Set<String> convertClassNameToServices(@NotNull String fqnClassName) {
Set<String> serviceNames = new HashSet<>();
fqnClassName = StringUtils.stripStart(fqnClassName, "\\");
for(Map.Entry<String, ContainerService> entry: this.getServices().entrySet()) {
for (String className : entry.getValue().getClassNames()) {
String indexedClassName = this.getParameterCollector().resolve(className);
if(indexedClassName != null) {
if(StringUtils.stripStart(indexedClassName, "\\").equalsIgnoreCase(fqnClassName)) {
serviceNames.add(entry.getKey());
}
}
}
}
return serviceNames;
}
private Set<String> getNames() {
Set<String> serviceNames = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
// local filesystem
serviceNames.addAll(ServiceXmlParserFactory.getInstance(project, XmlServiceParser.class).getServiceMap().getMap().keySet());
// Extension points
ServiceCollectorParameter.Id parameter = null;
for (fr.adrienbrault.idea.symfony2plugin.extension.ServiceCollector collectorEx : EXTENSIONS.getExtensions()) {
if(parameter == null) {
parameter = new ServiceCollectorParameter.Id(project, serviceNames);
}
collectorEx.collectIds(parameter);
}
// index
serviceNames.addAll(
FileIndexCaches.getIndexKeysCache(project, SERVICE_CONTAINER_INDEX_NAMES, ServicesDefinitionStubIndex.KEY)
);
return serviceNames;
}
private ParameterCollector getParameterCollector() {
return (this.parameterCollector != null) ? this.parameterCollector : (this.parameterCollector = ParameterCollector.create(this.project));
}
public static ServiceCollector create(@NotNull Project project) {
return new ContainerCollectionResolver.ServiceCollector(project);
}
}
public static class ParameterCollector {
@NotNull
private Project project;
@Nullable
private Map<String, ContainerParameter> containerParameterMap;
public ParameterCollector(@NotNull Project project) {
this.project = project;
}
public static ParameterCollector create(@NotNull Project project) {
return new ParameterCollector(project);
}
/**
*
* Resolve service class name which can be a class name or parameter, unknown parameter returns null
*
*/
@Nullable
private String resolve(@Nullable String paramOrClassName) {
if(paramOrClassName == null) {
return null;
}
// strip "%" to get the parameter name
if(paramOrClassName.length() > 1 && paramOrClassName.startsWith("%") && paramOrClassName.endsWith("%")) {
paramOrClassName = paramOrClassName.substring(1, paramOrClassName.length() - 1);
// parameter is always lower see #179
paramOrClassName = paramOrClassName.toLowerCase();
if(this.getParameters().containsKey(paramOrClassName)) {
return getParameters().get(paramOrClassName).getValue();
}
return null;
}
return paramOrClassName;
}
private Map<String, ContainerParameter> getParameters() {
if(this.containerParameterMap != null) {
return this.containerParameterMap;
}
this.containerParameterMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
// local filesystem
for(Map.Entry<String, String> Entry: ServiceXmlParserFactory.getInstance(project, ParameterServiceParser.class).getParameterMap().entrySet()) {
// user input here; secure nullable values
String key = Entry.getKey();
if(key != null) {
this.containerParameterMap.put(key, new ContainerParameter(key, Entry.getValue()));
}
}
// index
for (Map.Entry<String, List<String>> entry : FileIndexCaches.getStringDataCache(project, SERVICE_PARAMETER_INDEX, SERVICE_PARAMETER_INDEX_NAMES, ContainerParameterStubIndex.KEY, ServiceIndexUtil.getRestrictedFileTypesScope(project)).entrySet()) {
String parameterName = entry.getKey();
// just for secure
if(parameterName == null) {
continue;
}
// indexes is weak stuff, dont overwrite compiled ones
if(!this.containerParameterMap.containsKey(parameterName)) {
this.containerParameterMap.put(parameterName, new ContainerParameter(parameterName, entry.getValue(), true));
}
}
// setParameter("foo") for ContainerBuilder
for (ContainerBuilderCall call : FileBasedIndexImpl.getInstance().getValues(ContainerBuilderStubIndex.KEY, "setParameter", GlobalSearchScope.allScope(project))) {
Collection<String> parameters = call.getParameter();
if(parameters == null || parameters.size() == 0) {
continue;
}
for (String parameter : parameters) {
if(this.containerParameterMap.containsKey(parameter)) {
continue;
}
this.containerParameterMap.put(parameter, new ContainerParameter(parameter, true));
}
}
return this.containerParameterMap;
}
private Set<String> getNames() {
// use overall map if already generated
if(this.containerParameterMap != null) {
return this.containerParameterMap.keySet();
}
Set<String> parameterNames = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
// local filesystem
parameterNames.addAll(ServiceXmlParserFactory.getInstance(project, ParameterServiceParser.class).getParameterMap().keySet());
// index
parameterNames.addAll(
FileIndexCaches.getIndexKeysCache(project, SERVICE_PARAMETER_INDEX_NAMES, ContainerParameterStubIndex.KEY)
);
// setParameter("foo") for ContainerBuilder
for (ContainerBuilderCall call : FileBasedIndexImpl.getInstance().getValues(ContainerBuilderStubIndex.KEY, "setParameter", GlobalSearchScope.allScope(project))) {
Collection<String> parameter = call.getParameter();
if(parameter != null) {
parameterNames.addAll(parameter);
}
}
return parameterNames;
}
}
}