package fr.adrienbrault.idea.symfony2plugin.doctrine;
import com.intellij.openapi.util.Pair;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import com.jetbrains.php.lang.documentation.phpdoc.parser.PhpDocElementTypes;
import com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocComment;
import com.jetbrains.php.lang.documentation.phpdoc.psi.tags.PhpDocTag;
import com.jetbrains.php.lang.psi.PhpFile;
import com.jetbrains.php.lang.psi.elements.PhpClass;
import com.jetbrains.php.lang.psi.elements.PhpPsiElement;
import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.visitor.AnnotationElementWalkingVisitor;
import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils;
import fr.adrienbrault.idea.symfony2plugin.util.yaml.YamlHelper;
import org.apache.commons.lang.ArrayUtils;
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.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author Daniel Espendiller <daniel@espendiller.net>
*/
public class DoctrineUtil {
public static final String[] MODEL_CLASS_ANNOTATION = new String[]{
"\\Doctrine\\ORM\\Mapping\\Entity",
"\\TYPO3\\Flow\\Annotations\\Entity",
"\\Doctrine\\ODM\\MongoDB\\Mapping\\Annotations\\Document",
"\\Doctrine\\ODM\\CouchDB\\Mapping\\Annotations\\Document",
};
/**
* Index metadata file with its class and repository.
* As of often class stay in static only context
*/
@Nullable
public static Collection<Pair<String, String>> getClassRepositoryPair(@NotNull PsiFile psiFile) {
Collection<Pair<String, String>> pairs = null;
if(psiFile instanceof XmlFile) {
pairs = getClassRepositoryPair((XmlFile) psiFile);
} else if(psiFile instanceof YAMLFile) {
pairs = getClassRepositoryPair((YAMLFile) psiFile);
} else if(psiFile instanceof PhpFile) {
pairs = getClassRepositoryPair((PhpFile) psiFile);
}
return pairs;
}
/**
* Extract class and repository from xml meta files
* We support orm and all odm syntax here
*/
@Nullable
private static Collection<Pair<String, String>> getClassRepositoryPair(@NotNull XmlFile xmlFile) {
XmlTag rootTag = xmlFile.getRootTag();
if(rootTag == null || !rootTag.getName().toLowerCase().startsWith("doctrine")) {
return null;
}
Collection<Pair<String, String>> pairs = new ArrayList<>();
for (XmlTag xmlTag : (XmlTag[]) ArrayUtils.addAll(rootTag.findSubTags("document"), rootTag.findSubTags("entity"))) {
XmlAttribute attr = xmlTag.getAttribute("name");
if(attr == null) {
continue;
}
String value = attr.getValue();
if(StringUtils.isBlank(value)) {
continue;
}
// extract repository-class; allow nullable
String repositoryClass = null;
XmlAttribute repositoryClassAttribute = xmlTag.getAttribute("repository-class");
if(repositoryClassAttribute != null) {
String repositoryClassAttributeValue = repositoryClassAttribute.getValue();
if(StringUtils.isNotBlank(repositoryClassAttributeValue)) {
repositoryClass = repositoryClassAttributeValue;
}
}
pairs.add(Pair.create(value, repositoryClass));
}
if(pairs.size() == 0) {
return null;
}
return pairs;
}
/**
* Extract class and repository from all php annotations
* We support multiple use case like orm an so on
*/
@Nullable
private static Collection<Pair<String, String>> getClassRepositoryPair(@NotNull PhpFile phpFile) {
final Collection<Pair<String, String>> pairs = new ArrayList<>();
phpFile.acceptChildren(new AnnotationElementWalkingVisitor(phpDocTag -> {
PhpDocComment phpDocComment = PsiTreeUtil.getParentOfType(phpDocTag, PhpDocComment.class);
if (phpDocComment == null) {
return false;
}
PhpPsiElement phpClass = phpDocComment.getNextPsiSibling();
if (!(phpClass instanceof PhpClass)) {
return false;
}
PhpClass phpClassScope = (PhpClass) phpClass;
pairs.add(Pair.create(
phpClassScope.getPresentableFQN(),
getAnnotationRepositoryClass(phpDocTag, phpClassScope))
);
return false;
}, MODEL_CLASS_ANNOTATION));
return pairs;
}
/**
* Extract text: @Entity(repositoryClass="foo")
*/
@Nullable
private static String getAnnotationRepositoryClass(@NotNull PhpDocTag phpDocTag, @NotNull PhpClass phpClass) {
PsiElement phpDocAttributeList = PsiElementUtils.getChildrenOfType(phpDocTag, PlatformPatterns.psiElement(PhpDocElementTypes.phpDocAttributeList));
if(phpDocAttributeList == null) {
return null;
}
// repositoryClass="Foobar"
String text = phpDocAttributeList.getText();
Matcher matcher = Pattern.compile("repositoryClass\\s*=\\s*\"([^\"]*)\"").matcher(text);
if (matcher.find()) {
return matcher.group(1);
}
// repositoryClass=Foobar::class
// @TODO: use annotation plugin
matcher = Pattern.compile("repositoryClass\\s*=\\s*([^\\s:]*)::class").matcher(text);
if (matcher.find()) {
PhpClass classConstant = EntityHelper.getAnnotationRepositoryClass(phpClass, matcher.group(1));
if(classConstant != null) {
return StringUtils.stripStart(classConstant.getFQN(), "\\");
}
}
return null;
}
/**
* Extract class and repository from all yaml files
* We need to filter on some condition, so we dont index files which not holds meta for doctrine
*
* Note: index context method, so file validity in each statement
*/
@Nullable
private static Collection<Pair<String, String>> getClassRepositoryPair(@NotNull YAMLFile yamlFile) {
// we are indexing all yaml files for prefilter on path,
// if false if check on parse
String name = yamlFile.getName().toLowerCase();
boolean iAmMetadataFile = name.contains(".odm.") || name.contains(".orm.") || name.contains(".mongodb.") || name.contains(".couchdb.");
YAMLDocument yamlDocument = PsiTreeUtil.getChildOfType(yamlFile, YAMLDocument.class);
if(yamlDocument == null) {
return null;
}
YAMLValue topLevelValue = yamlDocument.getTopLevelValue();
if(!(topLevelValue instanceof YAMLMapping)) {
return null;
}
Collection<Pair<String, String>> pairs = new ArrayList<>();
for (YAMLKeyValue yamlKey : ((YAMLMapping) topLevelValue).getKeyValues()) {
String keyText = yamlKey.getKeyText();
if(StringUtils.isBlank(keyText)) {
continue;
}
String repositoryClass = YamlHelper.getYamlKeyValueAsString(yamlKey, "repositoryClass");
// fine repositoryClass exists a valid metadata file
if(!iAmMetadataFile && repositoryClass != null) {
iAmMetadataFile = true;
}
// currently not valid metadata file find valid keys
// else we are not allowed to store values
if(!iAmMetadataFile) {
Set<String> keySet = YamlHelper.getKeySet(yamlKey);
if(keySet == null) {
continue;
}
if(!(keySet.contains("fields") || keySet.contains("id") || keySet.contains("collection") || keySet.contains("db") || keySet.contains("indexes"))) {
continue;
}
iAmMetadataFile = true;
}
pairs.add(Pair.create(keyText, repositoryClass));
}
if(pairs.size() == 0) {
return null;
}
return pairs;
}
}