blob: af3c9a84071f427803dd13e680ec8883c5ec3a61 [file] [log] [blame]
/*
* Copyright 2000-2014 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 com.intellij.lang.ant.dom;
import com.intellij.lang.ant.AntIntrospector;
import com.intellij.lang.ant.ReflectedProject;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.pom.PomTarget;
import com.intellij.psi.CommonClassNames;
import com.intellij.psi.PsiFileSystemItem;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlElement;
import com.intellij.psi.xml.XmlTag;
import com.intellij.util.xml.*;
import com.intellij.util.xml.reflect.*;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.Reference;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*;
/**
* @author Eugene Zhuravlev
* Date: Apr 9, 2010
*/
public class AntDomExtender extends DomExtender<AntDomElement>{
private static final Logger LOG = Logger.getInstance("#com.intellij.lang.ant.dom.AntDomExtender");
private static final Key<Class> ELEMENT_IMPL_CLASS_KEY = Key.create("_element_impl_class_");
private static final Key<Boolean> IS_TASK_CONTAINER = Key.create("_task_container_");
private static final Map<String, Class<? extends AntDomElement>> TAG_MAPPING = new HashMap<String, Class<? extends AntDomElement>>();
static {
TAG_MAPPING.put("property", AntDomProperty.class);
TAG_MAPPING.put("dirname", AntDomDirname.class);
TAG_MAPPING.put("fileset", AntDomFileSet.class);
TAG_MAPPING.put("dirset", AntDomDirSet.class);
TAG_MAPPING.put("filelist", AntDomFileList.class);
TAG_MAPPING.put("pathelement", AntDomPathElement.class);
TAG_MAPPING.put("path", AntDomPath.class);
TAG_MAPPING.put("classpath", AntDomClasspath.class);
TAG_MAPPING.put("typedef", AntDomTypeDef.class);
TAG_MAPPING.put("taskdef", AntDomTaskdef.class);
TAG_MAPPING.put("presetdef", AntDomPresetDef.class);
TAG_MAPPING.put("macrodef", AntDomMacroDef.class);
TAG_MAPPING.put("scriptdef", AntDomScriptDef.class);
TAG_MAPPING.put("antlib", AntDomAntlib.class);
TAG_MAPPING.put("ant", AntDomAnt.class);
TAG_MAPPING.put("antcall", AntDomAntCall.class);
TAG_MAPPING.put("available", AntDomPropertyDefiningTaskWithDefaultValue.class);
TAG_MAPPING.put("condition", AntDomPropertyDefiningTaskWithDefaultValue.class);
TAG_MAPPING.put("uptodate", AntDomPropertyDefiningTaskWithDefaultValue.class);
TAG_MAPPING.put("checksum", AntDomChecksumTask.class);
TAG_MAPPING.put("loadfile", AntDomLoadFileTask.class);
TAG_MAPPING.put("whichresource", AntDomWhichResourceTask.class);
TAG_MAPPING.put("jarlib-resolve", AntDomPropertyDefiningTask.class);
TAG_MAPPING.put("p4counter", AntDomPropertyDefiningTask.class);
TAG_MAPPING.put("pathconvert", AntDomPropertyDefiningTask.class);
TAG_MAPPING.put("basename", AntDomBasenameTask.class);
TAG_MAPPING.put("length", AntDomLengthTask.class);
TAG_MAPPING.put("tempfile", AntDomTempFile.class);
TAG_MAPPING.put("exec", AntDomExecTask.class);
TAG_MAPPING.put("buildnumber", AntDomBuildnumberTask.class);
TAG_MAPPING.put("tstamp", AntDomTimestampTask.class);
TAG_MAPPING.put("format", AntDomTimestampTaskFormat.class);
TAG_MAPPING.put("input", AntDomInputTask.class);
}
public void registerExtensions(@NotNull final AntDomElement antDomElement, @NotNull DomExtensionsRegistrar registrar) {
final XmlElement xmlElement = antDomElement.getXmlElement();
if (xmlElement instanceof XmlTag) {
final XmlTag xmlTag = (XmlTag)xmlElement;
final String tagName = xmlTag.getName();
final AntDomProject antProject = antDomElement.getAntProject();
if (antProject == null) {
return;
}
final ReflectedProject reflected = ReflectedProject.getProject(antProject.getClassLoader());
if (reflected.getProject() == null) {
return;
}
final DomGenericInfo genericInfo = antDomElement.getGenericInfo();
AntIntrospector classBasedIntrospector = null;
final Hashtable<String,Class> coreTaskDefs = reflected.getTaskDefinitions();
final Hashtable<String, Class> coreTypeDefs = reflected.getDataTypeDefinitions();
final boolean isCustom = antDomElement instanceof AntDomCustomElement;
if ("project".equals(tagName)) {
classBasedIntrospector = getIntrospector(reflected.getProject().getClass());
}
else if ("target".equals(tagName)) {
classBasedIntrospector = getIntrospector(reflected.getTargetClass());
}
else {
if (isCustom) {
final AntDomCustomElement custom = (AntDomCustomElement)antDomElement;
final Class definitionClass = custom.getDefinitionClass();
if (definitionClass != null) {
classBasedIntrospector = getIntrospector(definitionClass);
}
}
else {
Class elemType = antDomElement.getChildDescription().getUserData(ELEMENT_IMPL_CLASS_KEY);
if (elemType == null) {
if (coreTaskDefs != null){
elemType = coreTaskDefs.get(tagName);
}
}
if (elemType == null) {
if (coreTypeDefs != null){
elemType = coreTypeDefs.get(tagName);
}
}
if (elemType != null) {
classBasedIntrospector = getIntrospector(elemType);
}
}
}
AbstractIntrospector parentIntrospector = null;
if (classBasedIntrospector != null) {
parentIntrospector = new ClassIntrospectorAdapter(classBasedIntrospector, coreTaskDefs, coreTypeDefs);
}
else {
if (isCustom) {
final AntDomNamedElement declaringElement = ((AntDomCustomElement)antDomElement).getDeclaringElement();
if (declaringElement instanceof AntDomMacroDef) {
parentIntrospector = new MacrodefIntrospectorAdapter((AntDomMacroDef)declaringElement);
}
else if (declaringElement instanceof AntDomMacrodefElement){
parentIntrospector = new MacrodefElementOccurrenceIntrospectorAdapter((AntDomMacrodefElement)declaringElement)/*ContainerElementIntrospector.INSTANCE*/;
}
else if (declaringElement instanceof AntDomScriptDef) {
parentIntrospector = new ScriptdefIntrospectorAdapter((AntDomScriptDef)declaringElement);
}
}
}
if (parentIntrospector != null) {
defineAttributes(xmlTag, registrar, genericInfo, parentIntrospector);
if ("project".equals(tagName) || parentIntrospector.isContainer()) { // can contain any task or/and type definition
if (coreTaskDefs != null) {
for (Map.Entry<String, Class> entry : coreTaskDefs.entrySet()) {
final DomExtension extension = registerChild(registrar, genericInfo, entry.getKey());
if (extension != null) {
final Class type = entry.getValue();
if (type != null) {
extension.putUserData(ELEMENT_IMPL_CLASS_KEY, type);
}
extension.putUserData(AntDomElement.ROLE, AntDomElement.Role.TASK);
}
}
}
if (coreTypeDefs != null) {
for (Map.Entry<String, Class> entry : coreTypeDefs.entrySet()) {
final DomExtension extension = registerChild(registrar, genericInfo, entry.getKey());
if (extension != null) {
final Class type = entry.getValue();
if (type != null) {
extension.putUserData(ELEMENT_IMPL_CLASS_KEY, type);
}
extension.putUserData(AntDomElement.ROLE, AntDomElement.Role.DATA_TYPE);
}
}
}
registrar.registerCustomChildrenExtension(AntDomCustomElement.class, new AntCustomTagNameDescriptor());
}
else {
final Iterator<String> nested = parentIntrospector.getNestedElementsIterator();
while (nested.hasNext()) {
final String nestedElementName = nested.next();
final DomExtension extension = registerChild(registrar, genericInfo, nestedElementName);
if (extension != null) {
Class type = parentIntrospector.getNestedElementType(nestedElementName);
if (type != null && CommonClassNames.JAVA_LANG_OBJECT.equals(type.getName())) {
type = null; // hack to support badly written tasks
}
if (type == null) {
if (coreTypeDefs != null){
type = coreTypeDefs.get(nestedElementName);
}
}
if (type != null) {
extension.putUserData(ELEMENT_IMPL_CLASS_KEY, type);
}
AntDomElement.Role role = AntDomElement.Role.DATA_TYPE;
if (coreTaskDefs != null && coreTaskDefs.containsKey(nestedElementName)) {
role = AntDomElement.Role.TASK;
}
else if (type != null && isAssignableFrom(Task.class.getName(), type)) {
role = AntDomElement.Role.TASK;
}
if (role != null) {
extension.putUserData(AntDomElement.ROLE, role);
}
}
}
registrar.registerCustomChildrenExtension(AntDomCustomElement.class, new AntCustomTagNameDescriptor());
}
}
}
}
private static void defineAttributes(XmlTag xmlTag, DomExtensionsRegistrar registrar, DomGenericInfo genericInfo, AbstractIntrospector parentIntrospector) {
final Map<String, Pair<Type, Class>> registeredAttribs = getStaticallyRegisteredAttributes(genericInfo);
// define attributes discovered by introspector and not yet defined statically
final Iterator<String> introspectedAttributes = parentIntrospector.getAttributesIterator();
while (introspectedAttributes.hasNext()) {
final String attribName = introspectedAttributes.next();
if (genericInfo.getAttributeChildDescription(attribName) == null) { // if not defined yet
final String _attribName = attribName.toLowerCase(Locale.US);
final Pair<Type, Class> types = registeredAttribs.get(_attribName);
Type type = types != null? types.getFirst() : null;
Class converterClass = types != null ? types.getSecond() : null;
if (type == null) {
type = String.class; // use String by default
final Class attributeType = parentIntrospector.getAttributeType(attribName);
if (attributeType != null) {
// handle well-known types
if (File.class.isAssignableFrom(attributeType)) {
type = PsiFileSystemItem.class;
converterClass = AntPathConverter.class;
}
else if (Boolean.class.isAssignableFrom(attributeType)){
type = Boolean.class;
converterClass = AntBooleanConverter.class;
}
else if (isAssignableFrom(Reference.class.getName(), attributeType)) {
converterClass = AntDomRefIdConverter.class;
}
}
}
LOG.assertTrue(type != null);
registerAttribute(registrar, attribName, type, converterClass);
if (types == null) { // augment the map if this was a newly added attribute
registeredAttribs.put(_attribName, Pair.create(type, converterClass));
}
}
}
// handle attribute case problems:
// additionaly register all attributes that exist in XML but differ from the registered ones only in case
for (XmlAttribute xmlAttribute : xmlTag.getAttributes()) {
final String existingAttribName = xmlAttribute.getName();
if (genericInfo.getAttributeChildDescription(existingAttribName) == null) {
final Pair<Type, Class> pair = registeredAttribs.get(existingAttribName.toLowerCase(Locale.US));
if (pair != null) { // if such attribute should actually be here
registerAttribute(registrar, existingAttribName, pair.getFirst(), pair.getSecond());
}
}
}
}
private static void registerAttribute(DomExtensionsRegistrar registrar, String attribName, final @NotNull Type attributeType, final @Nullable Class converterType) {
final DomExtension extension = registrar.registerGenericAttributeValueChildExtension(new XmlName(attribName), attributeType);
if (converterType != null) {
try {
extension.setConverter((Converter)converterType.newInstance());
}
catch (InstantiationException e) {
LOG.info(e);
}
catch (IllegalAccessException e) {
LOG.info(e);
}
}
}
private static Map<String, Pair<Type, Class>> getStaticallyRegisteredAttributes(final DomGenericInfo genericInfo) {
final Map<String, Pair<Type, Class>> map = new HashMap<String, Pair<Type, Class>>();
for (DomAttributeChildDescription description : genericInfo.getAttributeChildrenDescriptions()) {
final Type type = description.getType();
if (type instanceof ParameterizedType) {
final Type[] typeArguments = ((ParameterizedType)type).getActualTypeArguments();
if (typeArguments.length == 1) {
String name = description.getXmlElementName();
final Type attribType = typeArguments[0];
Class<? extends Converter> converterType = null;
final Convert converterAnnotation = description.getAnnotation(Convert.class);
if (converterAnnotation != null) {
converterType = converterAnnotation.value();
}
map.put(name.toLowerCase(Locale.US), new Pair<Type, Class>(attribType, converterType));
}
}
}
return map;
}
@Nullable
private static DomExtension registerChild(DomExtensionsRegistrar registrar, DomGenericInfo elementInfo, String childName) {
if (elementInfo.getCollectionChildDescription(childName) == null) { // register if not yet defined statically
Class<? extends AntDomElement> modelClass = getModelClass(childName);
if (modelClass == null) {
modelClass = AntDomElement.class;
}
return registrar.registerCollectionChildrenExtension(new XmlName(childName), modelClass);
}
return null;
}
@Nullable
public static AntIntrospector getIntrospector(Class c) {
try {
return AntIntrospector.getInstance(c);
}
catch (Throwable ignored) {
}
return null;
}
@Nullable
private static Class<? extends AntDomElement> getModelClass(@NotNull String tagName) {
return TAG_MAPPING.get(tagName.toLowerCase(Locale.US));
}
private static boolean isAssignableFrom(final String baseClassName, final Class clazz) {
try {
final ClassLoader loader = clazz.getClassLoader();
if (loader != null) {
final Class baseClass = loader.loadClass(baseClassName);
return baseClass.isAssignableFrom(clazz);
}
}
catch (ClassNotFoundException ignored) {
}
return false;
}
private static class AntCustomTagNameDescriptor extends CustomDomChildrenDescription.TagNameDescriptor {
public Set<EvaluatedXmlName> getCompletionVariants(@NotNull DomElement parent) {
if (!(parent instanceof AntDomElement)) {
return Collections.emptySet();
}
final AntDomElement element = (AntDomElement)parent;
final AntDomProject antDomProject = element.getAntProject();
if (antDomProject == null) {
return Collections.emptySet();
}
final CustomAntElementsRegistry registry = CustomAntElementsRegistry.getInstance(antDomProject);
final Set<EvaluatedXmlName> result = new HashSet<EvaluatedXmlName>();
for (XmlName variant : registry.getCompletionVariants(element)) {
final String ns = variant.getNamespaceKey();
result.add(new DummyEvaluatedXmlName(variant, ns != null? ns : ""));
}
return result;
}
@Nullable
public PomTarget findDeclaration(DomElement parent, @NotNull EvaluatedXmlName name) {
final XmlName xmlName = name.getXmlName();
return doFindDeclaration(parent, xmlName);
}
@Nullable
public PomTarget findDeclaration(@NotNull DomElement child) {
XmlName name = new XmlName(child.getXmlElementName(), child.getXmlElementNamespace());
return doFindDeclaration(child.getParent(), name);
}
@Nullable
private static PomTarget doFindDeclaration(DomElement parent, XmlName xmlName) {
if (!(parent instanceof AntDomElement)) {
return null;
}
final AntDomElement parentElement = (AntDomElement)parent;
final AntDomProject antDomProject = parentElement.getAntProject();
if (antDomProject == null) {
return null;
}
final CustomAntElementsRegistry registry = CustomAntElementsRegistry.getInstance(antDomProject);
final AntDomElement declaringElement = registry.findDeclaringElement(parentElement, xmlName);
if (declaringElement == null) {
return null;
}
DomTarget target = DomTarget.getTarget(declaringElement);
if (target == null && declaringElement instanceof AntDomTypeDef) {
final AntDomTypeDef typedef = (AntDomTypeDef)declaringElement;
final GenericAttributeValue<PsiFileSystemItem> resource = typedef.getResource();
if (resource != null) {
target = DomTarget.getTarget(declaringElement, resource);
}
if (target == null) {
final GenericAttributeValue<PsiFileSystemItem> file = typedef.getFile();
if (file != null) {
target = DomTarget.getTarget(declaringElement, file);
}
}
}
return target;
}
}
private static abstract class AbstractIntrospector {
@NotNull
public Iterator<String> getAttributesIterator() {
return Collections.<String>emptyList().iterator();
}
@NotNull
public Iterator<String> getNestedElementsIterator(){
return Collections.<String>emptyList().iterator();
}
public abstract boolean isContainer();
@Nullable
public Class getAttributeType(String attribName) {
return null;
}
@Nullable
public Class getNestedElementType(String elementName) {
return null;
}
}
private static class ClassIntrospectorAdapter extends AbstractIntrospector {
private final AntIntrospector myIntrospector;
private final Map<String, Class> myCoreTaskDefs;
private final Map<String, Class> myCoreTypeDefs;
private List<String> myNestedElements;
private Map<String, Class> myNestedElementTypes;
private ClassIntrospectorAdapter(AntIntrospector introspector) {
this(introspector, null, null);
}
public ClassIntrospectorAdapter(AntIntrospector introspector, Map<String, Class> coreTaskDefs, Map<String, Class> coreTypeDefs) {
myIntrospector = introspector;
myCoreTaskDefs = coreTaskDefs != null? coreTaskDefs : Collections.<String, Class>emptyMap();
myCoreTypeDefs = coreTypeDefs != null? coreTypeDefs : Collections.<String, Class>emptyMap();
}
@NotNull
public Iterator<String> getAttributesIterator() {
return new EnumerationToIteratorAdapter<String>(myIntrospector.getAttributes());
}
public Class getAttributeType(String attribName) {
return myIntrospector.getAttributeType(attribName);
}
public boolean isContainer() {
return myIntrospector.isContainer();
}
@NotNull
public Iterator<String> getNestedElementsIterator() {
initNestedElements();
return myNestedElements.iterator();
}
public Class getNestedElementType(String attribName) {
initNestedElements();
return myNestedElementTypes.get(attribName);
}
private void initNestedElements() {
if (myNestedElements != null) {
return;
}
myNestedElements = new ArrayList<String>();
myNestedElementTypes = new HashMap<String, Class>();
final Enumeration<String> nestedElements = myIntrospector.getNestedElements();
while (nestedElements.hasMoreElements()) {
final String elemName = nestedElements.nextElement();
myNestedElements.add(elemName);
myNestedElementTypes.put(elemName, myIntrospector.getElementType(elemName));
}
final Set<String> extensionPointTypes = myIntrospector.getExtensionPointTypes();
for (String extPoint : extensionPointTypes) {
processEntries(extPoint, myCoreTaskDefs);
processEntries(extPoint, myCoreTypeDefs);
}
}
private void processEntries(String extPoint, final Map<String, Class> definitions) {
for (Map.Entry<String, Class> entry : definitions.entrySet()) {
final String elementName = entry.getKey();
final Class taskClass = entry.getValue();
if (isAssignableFrom(extPoint, taskClass)) {
myNestedElements.add(elementName);
myNestedElementTypes.put(elementName, taskClass);
}
}
}
}
private static class MacrodefIntrospectorAdapter extends AbstractIntrospector {
private final AntDomMacroDef myMacrodef;
private MacrodefIntrospectorAdapter(AntDomMacroDef macrodef) {
myMacrodef = macrodef;
}
@NotNull
public Iterator<String> getAttributesIterator() {
final List<AntDomMacrodefAttribute> macrodefAttributes = myMacrodef.getMacroAttributes();
if (macrodefAttributes.size() == 0) {
return Collections.<String>emptyList().iterator();
}
final List<String> attribs = new ArrayList<String>(macrodefAttributes.size());
for (AntDomMacrodefAttribute attribute : macrodefAttributes) {
final String attribName = attribute.getName().getRawText();
if (attribName != null) {
attribs.add(attribName);
}
}
return attribs.iterator();
}
public boolean isContainer() {
for (AntDomMacrodefElement element : myMacrodef.getMacroElements()) {
final GenericAttributeValue<Boolean> implicit = element.isImplicit();
if (implicit != null && Boolean.TRUE.equals(implicit.getValue())) {
return true;
}
}
return false;
}
}
private static class MacrodefElementOccurrenceIntrospectorAdapter extends AbstractIntrospector {
private final AntDomMacrodefElement myElement;
private volatile List<AbstractIntrospector> myContexts;
private volatile Map<String, Class> myChildrenMap;
private MacrodefElementOccurrenceIntrospectorAdapter(AntDomMacrodefElement element) {
myElement = element;
}
public boolean isContainer() {
final List<AbstractIntrospector> contexts = getContexts();
for (AbstractIntrospector context : contexts) {
if (!context.isContainer()) {
return false;
}
}
return true;
}
@NotNull
public Iterator<String> getNestedElementsIterator() {
return getNestedElementsMap().keySet().iterator();
}
public Class getNestedElementType(String elementName) {
return getNestedElementsMap().get(elementName);
}
private Map<String, Class> getNestedElementsMap() {
if (myChildrenMap != null) {
return myChildrenMap;
}
final List<AbstractIntrospector> contexts = getContexts();
Map<String, Class> names = null;
for (AbstractIntrospector context : contexts) {
if (context.isContainer()) {
continue;
}
final Set<String> set = new HashSet<String>();
for (Iterator<String> it = context.getNestedElementsIterator();it.hasNext();) {
final String name = it.next();
set.add(name);
}
if (names == null) {
names = new HashMap<String, Class>();
for (String s : set) {
names.put(s, context.getNestedElementType(s));
}
}
else {
names.keySet().retainAll(set);
}
}
final Map<String, Class> result = names == null ? Collections.<String, Class>emptyMap() : names;
return myChildrenMap = result;
}
private List<AbstractIntrospector> getContexts() {
if (myContexts != null) {
return myContexts;
}
final List<AbstractIntrospector> parents = new ArrayList<AbstractIntrospector>();
final AntDomMacroDef macroDef = myElement.getParentOfType(AntDomMacroDef.class, true);
if (macroDef != null) {
final AntDomSequentialTask body = macroDef.getMacroBody();
if (body != null) {
body.accept(new AntDomRecursiveVisitor() {
public void visitAntDomCustomElement(AntDomCustomElement custom) {
if (myElement.equals(custom.getDeclaringElement())) {
final AntDomElement parent = custom.getParentOfType(AntDomElement.class, true);
if (parent != null) {
final Class type = parent.getChildDescription().getUserData(ELEMENT_IMPL_CLASS_KEY);
if (type != null) {
final AntIntrospector antIntrospector = AntIntrospector.getInstance(type);
if (antIntrospector != null) {
parents.add(new ClassIntrospectorAdapter(antIntrospector));
}
}
}
}
}
});
}
}
return myContexts = parents;
}
}
private static class ScriptdefIntrospectorAdapter extends AbstractIntrospector {
private final AntDomScriptDef myScriptDef;
private ScriptdefIntrospectorAdapter(AntDomScriptDef scriptDef) {
myScriptDef = scriptDef;
}
@NotNull
public Iterator<String> getAttributesIterator() {
final List<AntDomScriptdefAttribute> macrodefAttributes = myScriptDef.getScriptdefAttributes();
final List<String> attribs = new ArrayList<String>(macrodefAttributes.size());
for (AntDomScriptdefAttribute attribute : macrodefAttributes) {
final String nameAttrib = attribute.getName().getRawText();
if (nameAttrib != null) {
attribs.add(nameAttrib);
}
}
return attribs.iterator();
}
public boolean isContainer() {
return false;
}
}
private static class ContainerElementIntrospector extends AbstractIntrospector{
public static final ContainerElementIntrospector INSTANCE = new ContainerElementIntrospector();
public boolean isContainer() {
return true;
}
}
private static class EnumerationToIteratorAdapter<T> implements Iterator<T> {
private final Enumeration<T> myEnum;
public EnumerationToIteratorAdapter(Enumeration<T> enumeration) {
myEnum = enumeration;
}
public boolean hasNext() {
return myEnum.hasMoreElements();
}
public T next() {
return myEnum.nextElement();
}
public void remove() {
throw new UnsupportedOperationException("remove is not supported");
}
}
}