| /* |
| * Copyright 2000-2010 JetBrains s.r.o. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.jetbrains.android.dom.converters; |
| |
| import com.android.SdkConstants; |
| import com.android.resources.ResourceType; |
| import com.intellij.codeInspection.LocalQuickFix; |
| import com.intellij.openapi.components.ServiceManager; |
| import com.intellij.openapi.module.Module; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.psi.PsiElement; |
| import com.intellij.psi.PsiReference; |
| import com.intellij.psi.xml.XmlAttribute; |
| import com.intellij.psi.xml.XmlElement; |
| import com.intellij.psi.xml.XmlTag; |
| import com.intellij.util.xml.*; |
| import org.jetbrains.android.dom.AdditionalConverter; |
| import org.jetbrains.android.dom.AndroidResourceType; |
| import org.jetbrains.android.dom.resources.ResourceValue; |
| import org.jetbrains.android.facet.AndroidFacet; |
| import org.jetbrains.android.inspections.CreateFileResourceQuickFix; |
| import org.jetbrains.android.inspections.CreateValueResourceQuickFix; |
| import org.jetbrains.android.resourceManagers.FileResourceProcessor; |
| import org.jetbrains.android.resourceManagers.LocalResourceManager; |
| import org.jetbrains.android.resourceManagers.ResourceManager; |
| import org.jetbrains.android.util.AndroidResourceUtil; |
| import org.jetbrains.annotations.NonNls; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.util.*; |
| |
| import static com.android.SdkConstants.*; |
| import static org.jetbrains.android.util.AndroidUtils.SYSTEM_RESOURCE_PACKAGE; |
| |
| /** |
| * @author yole |
| */ |
| public class ResourceReferenceConverter extends ResolvingConverter<ResourceValue> |
| implements CustomReferenceConverter<ResourceValue>, AttributeValueDocumentationProvider { |
| private final List<String> myResourceTypes; |
| private ResolvingConverter<String> myAdditionalConverter; |
| private boolean myAdditionalConverterSoft = false; |
| private boolean myWithPrefix = true; |
| private boolean myWithExplicitResourceType = true; |
| private boolean myQuiet = false; |
| private boolean myAllowAttributeReferences = true; |
| private boolean myAllowLiterals = true; |
| |
| public ResourceReferenceConverter() { |
| this(new ArrayList<String>()); |
| } |
| |
| public ResourceReferenceConverter(@NotNull Collection<String> resourceTypes) { |
| myResourceTypes = new ArrayList<String>(resourceTypes); |
| } |
| |
| public void setAllowLiterals(boolean allowLiterals) { |
| myAllowLiterals = allowLiterals; |
| } |
| |
| public ResourceReferenceConverter(@NotNull String resourceType, boolean withPrefix, boolean withExplicitResourceType) { |
| myResourceTypes = Arrays.asList(resourceType); |
| myWithPrefix = withPrefix; |
| myWithExplicitResourceType = withExplicitResourceType; |
| } |
| |
| public void setAdditionalConverter(@Nullable ResolvingConverter<String> additionalConverter, boolean soft) { |
| myAdditionalConverter = additionalConverter; |
| myAdditionalConverterSoft = soft; |
| } |
| |
| public void setQuiet(boolean quiet) { |
| myQuiet = quiet; |
| } |
| |
| public void setAllowAttributeReferences(boolean allowAttributeReferences) { |
| myAllowAttributeReferences = allowAttributeReferences; |
| } |
| |
| @NotNull |
| private String getPackagePrefix(@Nullable String resourcePackage) { |
| String prefix = myWithPrefix ? "@" : ""; |
| if (resourcePackage == null) return prefix; |
| return prefix + resourcePackage + ':'; |
| } |
| |
| @Nullable |
| static String getValue(XmlElement element) { |
| if (element instanceof XmlAttribute) { |
| return ((XmlAttribute)element).getValue(); |
| } |
| else if (element instanceof XmlTag) { |
| return ((XmlTag)element).getValue().getText(); |
| } |
| return null; |
| } |
| |
| @Override |
| @NotNull |
| public Collection<? extends ResourceValue> getVariants(ConvertContext context) { |
| Set<ResourceValue> result = new HashSet<ResourceValue>(); |
| Module module = context.getModule(); |
| if (module == null) return result; |
| AndroidFacet facet = AndroidFacet.getInstance(module); |
| if (facet == null) return result; |
| |
| final Set<String> recommendedTypes = getResourceTypes(context); |
| |
| // hack to check if it is a real id attribute |
| if (recommendedTypes.contains(ResourceType.ID.getName()) && recommendedTypes.size() == 1) { |
| result.add(ResourceValue.reference(SdkConstants.NEW_ID_PREFIX)); |
| } |
| |
| XmlElement element = context.getXmlElement(); |
| if (element == null) return result; |
| String value = getValue(element); |
| assert value != null; |
| |
| if (!myQuiet || StringUtil.startsWithChar(value, '@')) { |
| String resourcePackage = null; |
| String systemPrefix = getPackagePrefix(SYSTEM_RESOURCE_PACKAGE); |
| if (value.startsWith(systemPrefix)) { |
| resourcePackage = SYSTEM_RESOURCE_PACKAGE; |
| } |
| else { |
| result.add(ResourceValue.literal(systemPrefix)); |
| } |
| final char prefix = myWithPrefix ? '@' : 0; |
| |
| if (value.startsWith(SdkConstants.NEW_ID_PREFIX)) { |
| addVariantsForIdDeclaration(result, facet, prefix, value); |
| } |
| |
| if (recommendedTypes.size() == 1) { |
| String type = recommendedTypes.iterator().next(); |
| boolean explicitResourceType = value.startsWith(getTypePrefix(resourcePackage, type)) || myWithExplicitResourceType; |
| addResourceReferenceValues(facet, prefix, type, resourcePackage, result, explicitResourceType); |
| } |
| else { |
| final Set<String> filteringSet = SYSTEM_RESOURCE_PACKAGE.equals(resourcePackage) |
| ? null |
| : getResourceTypesInCurrentModule(facet); |
| |
| for (ResourceType resourceType : ResourceType.values()) { |
| final String type = resourceType.getName(); |
| String typePrefix = getTypePrefix(resourcePackage, type); |
| if (value.startsWith(typePrefix)) { |
| addResourceReferenceValues(facet, prefix, type, resourcePackage, result, true); |
| } |
| else if (recommendedTypes.contains(type) && |
| (filteringSet == null || filteringSet.contains(type))) { |
| result.add(ResourceValue.literal(typePrefix)); |
| } |
| } |
| } |
| } |
| if (myAllowAttributeReferences) { |
| completeAttributeReferences(value, facet, result); |
| } |
| final ResolvingConverter<String> additionalConverter = getAdditionalConverter(context); |
| |
| if (additionalConverter != null) { |
| for (String variant : additionalConverter.getVariants(context)) { |
| result.add(ResourceValue.literal(variant)); |
| } |
| } |
| return result; |
| } |
| |
| private void addVariantsForIdDeclaration(Set<ResourceValue> result, AndroidFacet facet, char prefix, String value) { |
| for (String name : facet.getLocalResourceManager().getIds(false)) { |
| final ResourceValue ref = referenceTo(prefix, "+id", null, name, true); |
| |
| if (!value.startsWith(doToString(ref))) { |
| result.add(ref); |
| } |
| } |
| } |
| |
| private static void completeAttributeReferences(String value, AndroidFacet facet, Set<ResourceValue> result) { |
| if (StringUtil.startsWith(value, "?attr/")) { |
| addResourceReferenceValues(facet, '?', ResourceType.ATTR.getName(), null, result, true); |
| } |
| else if (StringUtil.startsWith(value, "?android:attr/")) { |
| addResourceReferenceValues(facet, '?', ResourceType.ATTR.getName(), SYSTEM_RESOURCE_PACKAGE, result, true); |
| } |
| else if (StringUtil.startsWithChar(value, '?')) { |
| addResourceReferenceValues(facet, '?', ResourceType.ATTR.getName(), null, result, false); |
| addResourceReferenceValues(facet, '?', ResourceType.ATTR.getName(), SYSTEM_RESOURCE_PACKAGE, result, false); |
| result.add(ResourceValue.literal("?attr/")); |
| result.add(ResourceValue.literal("?android:attr/")); |
| } |
| } |
| |
| @NotNull |
| public static Set<String> getResourceTypesInCurrentModule(@NotNull AndroidFacet facet) { |
| final Set<String> result = new HashSet<String>(); |
| final LocalResourceManager manager = facet.getLocalResourceManager(); |
| |
| manager.processFileResources(null, new FileResourceProcessor() { |
| @Override |
| public boolean process(@NotNull VirtualFile resFile, @NotNull String resName, @NotNull String resFolderType) { |
| if (ResourceType.getEnum(resFolderType) != null) { |
| result.add(resFolderType); |
| } |
| return true; |
| } |
| }); |
| |
| result.addAll(manager.getValueResourceTypes()); |
| if (manager.getIds(true).size() > 0) { |
| result.add(ResourceType.ID.getName()); |
| } |
| return result; |
| } |
| |
| @NotNull |
| private String getTypePrefix(String resourcePackage, String type) { |
| String typePart = type + '/'; |
| return getPackagePrefix(resourcePackage) + typePart; |
| } |
| |
| private Set<String> getResourceTypes(ConvertContext context) { |
| return getResourceTypes(context.getInvocationElement()); |
| } |
| |
| @NotNull |
| public Set<String> getResourceTypes(@NotNull DomElement element) { |
| AndroidResourceType resourceType = element.getAnnotation(AndroidResourceType.class); |
| Set<String> types = new HashSet<String>(myResourceTypes); |
| if (resourceType != null) { |
| String s = resourceType.value(); |
| if (s != null) types.add(s); |
| } |
| if (types.size() == 0) { |
| types.addAll(AndroidResourceUtil.getNames(AndroidResourceUtil.VALUE_RESOURCE_TYPES)); |
| } |
| else if (types.contains(ResourceType.DRAWABLE.getName())) { |
| types.add(ResourceType.COLOR.getName()); |
| } |
| return types; |
| } |
| |
| private static void addResourceReferenceValues(AndroidFacet facet, |
| char prefix, |
| String type, |
| @Nullable String resPackage, |
| Collection<ResourceValue> result, |
| boolean explicitResourceType) { |
| final ResourceManager manager = facet.getResourceManager(resPackage); |
| if (manager != null) { |
| for (String name : manager.getResourceNames(type)) { |
| result.add(referenceTo(prefix, type, resPackage, name, explicitResourceType)); |
| } |
| } |
| } |
| |
| private static ResourceValue referenceTo(char prefix, String type, String resPackage, String name, boolean explicitResourceType) { |
| return ResourceValue.referenceTo(prefix, resPackage, explicitResourceType ? type : null, name); |
| } |
| |
| @Override |
| public String getErrorMessage(@Nullable String s, ConvertContext context) { |
| if (s == null || s.isEmpty()) { |
| return "Missing value"; |
| } |
| |
| final ResourceValue parsed = ResourceValue.parse(s, true, myWithPrefix, false); |
| |
| if (parsed == null || !parsed.isReference()) { |
| final ResolvingConverter<String> additionalConverter = getAdditionalConverter(context); |
| |
| if (additionalConverter != null) { |
| return additionalConverter.getErrorMessage(s, context); |
| } |
| } else { |
| String errorMessage = parsed.getErrorMessage(); |
| if (errorMessage != null) { |
| return errorMessage; |
| } |
| } |
| |
| return super.getErrorMessage(s, context); |
| } |
| |
| @Override |
| public ResourceValue fromString(@Nullable @NonNls String s, ConvertContext context) { |
| if (s == null) return null; |
| ResourceValue parsed = ResourceValue.parse(s, true, myWithPrefix, true); |
| final ResolvingConverter<String> additionalConverter = getAdditionalConverter(context); |
| |
| if (parsed == null || !parsed.isReference()) { |
| if (additionalConverter != null) { |
| String value = additionalConverter.fromString(s, context); |
| if (value != null) { |
| return ResourceValue.literal(value); |
| } |
| else if (!myAdditionalConverterSoft) { |
| return null; |
| } |
| } |
| else if (!myAllowLiterals) { |
| return null; |
| } |
| } |
| if (parsed != null) { |
| final String resType = parsed.getResourceType(); |
| |
| if (parsed.getPrefix() == '?') { |
| if (!myAllowAttributeReferences) { |
| return null; |
| } |
| if (resType == null) { |
| parsed.setResourceType(ResourceType.ATTR.getName()); |
| } |
| else if (!ResourceType.ATTR.getName().equals(resType)) { |
| return null; |
| } |
| } |
| else if (resType == null && parsed.isReference()) { |
| if (myWithExplicitResourceType && !NULL_RESOURCE.equals(s)) { |
| return null; |
| } |
| if (myResourceTypes.size() == 1) { |
| parsed.setResourceType(myResourceTypes.get(0)); |
| } |
| } |
| } |
| return parsed; |
| } |
| |
| @Nullable |
| private ResolvingConverter<String> getAdditionalConverter(ConvertContext context) { |
| if (myAdditionalConverter != null) { |
| return myAdditionalConverter; |
| } |
| |
| final AdditionalConverter additionalConverterAnnotation = |
| context.getInvocationElement().getAnnotation(AdditionalConverter.class); |
| |
| if (additionalConverterAnnotation != null) { |
| final Class<? extends ResolvingConverter> converterClass = additionalConverterAnnotation.value(); |
| |
| if (converterClass != null) { |
| final ConverterManager converterManager = ServiceManager.getService(ConverterManager.class); |
| //noinspection unchecked |
| return (ResolvingConverter<String>)converterManager.getConverterInstance(converterClass); |
| } |
| } |
| return null; |
| } |
| |
| @Override |
| public String toString(@Nullable ResourceValue element, ConvertContext context) { |
| return doToString(element); |
| } |
| |
| private String doToString(ResourceValue element) { |
| if (element == null) { |
| return null; |
| } |
| if (myWithExplicitResourceType || !element.isReference()) { |
| return element.toString(); |
| } |
| return ResourceValue.referenceTo(element.getPrefix(), element.getPackage(), null, |
| element.getResourceName()).toString(); |
| } |
| |
| @Override |
| public LocalQuickFix[] getQuickFixes(ConvertContext context) { |
| AndroidFacet facet = AndroidFacet.getInstance(context); |
| if (facet != null) { |
| final DomElement domElement = context.getInvocationElement(); |
| |
| if (domElement instanceof GenericDomValue) { |
| final String value = ((GenericDomValue)domElement).getStringValue(); |
| |
| if (value != null) { |
| ResourceValue resourceValue = ResourceValue.parse(value, false, myWithPrefix, true); |
| if (resourceValue != null) { |
| String aPackage = resourceValue.getPackage(); |
| ResourceType resType = resourceValue.getType(); |
| if (resType == null && myResourceTypes.size() == 1) { |
| resType = ResourceType.getEnum(myResourceTypes.get(0)); |
| } |
| final String resourceName = resourceValue.getResourceName(); |
| if (aPackage == null && |
| resType != null && |
| resourceName != null && |
| AndroidResourceUtil.isCorrectAndroidResourceName(resourceName)) { |
| final List<LocalQuickFix> fixes = new ArrayList<LocalQuickFix>(); |
| |
| if (AndroidResourceUtil.XML_FILE_RESOURCE_TYPES.contains(resType)) { |
| fixes.add(new CreateFileResourceQuickFix(facet, resType, resourceName, context.getFile(), false)); |
| } |
| if (AndroidResourceUtil.VALUE_RESOURCE_TYPES.contains(resType) && resType != ResourceType.LAYOUT) { // layouts: aliases only |
| fixes.add(new CreateValueResourceQuickFix(facet, resType, resourceName, context.getFile(), false)); |
| } |
| return fixes.toArray(new LocalQuickFix[fixes.size()]); |
| } |
| } |
| } |
| } |
| } |
| return LocalQuickFix.EMPTY_ARRAY; |
| } |
| |
| @Override |
| @NotNull |
| public PsiReference[] createReferences(GenericDomValue<ResourceValue> value, PsiElement element, ConvertContext context) { |
| if (NULL_RESOURCE.equals(value.getStringValue())) { |
| return PsiReference.EMPTY_ARRAY; |
| } |
| |
| Module module = context.getModule(); |
| if (module != null) { |
| AndroidFacet facet = AndroidFacet.getInstance(module); |
| if (facet != null) { |
| ResourceValue resValue = value.getValue(); |
| if (resValue != null && resValue.isReference()) { |
| String resType = resValue.getResourceType(); |
| if (resType == null) { |
| return PsiReference.EMPTY_ARRAY; |
| } |
| |
| // Don't treat "+id" as a reference if it is actually defining an id locally; e.g. |
| // android:layout_alignLeft="@+id/foo" |
| // is a reference to R.id.foo, but |
| // android:id="@+id/foo" |
| // is not; it's the place we're defining it. |
| if (resValue.getPackage() == null && "+id".equals(resType) |
| && element != null && element.getParent() instanceof XmlAttribute) { |
| XmlAttribute attribute = (XmlAttribute)element.getParent(); |
| if (ATTR_ID.equals(attribute.getLocalName()) && ANDROID_URI.equals(attribute.getNamespace())) { |
| // When defining an id, don't point to another reference |
| // TODO: Unless you use @id instead of @+id! |
| return PsiReference.EMPTY_ARRAY; |
| } |
| } |
| |
| return new PsiReference[]{new AndroidResourceReference(value, facet, resValue, null)}; |
| } |
| } |
| } |
| return PsiReference.EMPTY_ARRAY; |
| } |
| |
| @Override |
| public String getDocumentation(@NotNull String value) { |
| return myAdditionalConverter instanceof AttributeValueDocumentationProvider |
| ? ((AttributeValueDocumentationProvider)myAdditionalConverter).getDocumentation(value) |
| : null; |
| } |
| } |