001// Licensed under the Apache License, Version 2.0 (the "License");
002// you may not use this file except in compliance with the License.
003// You may obtain a copy of the License at
004//
005// http://www.apache.org/licenses/LICENSE-2.0
006//
007// Unless required by applicable law or agreed to in writing, software
008// distributed under the License is distributed on an "AS IS" BASIS,
009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
010// See the License for the specific language governing permissions and
011// limitations under the License.
012
013package org.apache.tapestry5.internal.plastic;
014
015import java.util.Map;
016import java.util.Set;
017
018/**
019 * Used to track which methods are implemented by a base class, which is often needed when transforming
020 * a subclass.
021 */
022public class InheritanceData
023{
024    private final InheritanceData parent;
025
026    private final String packageName;
027
028    private final Set<String> methodNames = PlasticInternalUtils.newSet();
029    private final Map<String, Boolean> methods = PlasticInternalUtils.newMap();
030    private final Set<String> interfaceNames = PlasticInternalUtils.newSet();
031
032    public InheritanceData(String packageName)
033    {
034        this(null, packageName);
035    }
036
037    private InheritanceData(InheritanceData parent, String packageName)
038    {
039        this.parent = parent;
040        this.packageName = packageName;
041    }
042
043    /**
044     * Is this bundle for a transformed class, or for a base class (typically Object)?
045     *
046     * @return true if this bundle is for transformed class, false otherwise
047     */
048    public boolean isTransformed()
049    {
050        return parent != null;
051    }
052
053    /**
054     * Returns a new MethodBundle that represents the methods of a child class
055     * of this bundle. The returned bundle will always be {@linkplain #isTransformed() transformed}.
056     *
057     * @param packageName
058     *         the package that the child class will be created in
059     * @return new method bundle
060     */
061    public InheritanceData createChild(String packageName)
062    {
063        return new InheritanceData(this, packageName);
064    }
065
066    /**
067     * Adds a new instance method. Only non-private methods should be added (that is, methods which might
068     * be overridden in subclasses). This can later be queried to see if any base class implements the method.
069     *
070     * @param name
071     *         name of method
072     * @param desc
073     *         describes the parameters and return value of the method
074     * @param samePackageOnly
075     *         whether the method can only be overridden in classes that are in the same package
076     */
077    public void addMethod(String name, String desc, boolean samePackageOnly)
078    {
079        methods.put(toValue(name, desc), samePackageOnly);
080        methodNames.add(name);
081    }
082
083
084    /**
085     * Returns true if this class or a transformed parent class contains an implementation of,
086     * or abstract placeholder for, the method.
087     *
088     * @param name
089     *         method name
090     * @param desc
091     *         method descriptor
092     * @return true if this class or a base class implements the method (including abstract methods)
093     */
094    public boolean isImplemented(String name, String desc)
095    {
096        return checkForMethod(toValue(name, desc), this);
097    }
098
099    /**
100     * Returns true if the method is an override of a base class method
101     *
102     * @param name
103     *         method name
104     * @param desc
105     *         method descriptor
106     * @return true if a base class implements the method (including abstract methods)
107     */
108    public boolean isOverride(String name, String desc)
109    {
110        return checkForMethod(toValue(name, desc), parent);
111    }
112
113    private boolean checkForMethod(String value, InheritanceData cursor)
114    {
115
116        String thisPackageName = packageName;
117
118        while (cursor != null)
119        {
120            if (cursor.methods.containsKey(value))
121            {
122                boolean mustBeInSamePackage = cursor.methods.get(value);
123
124                if (!mustBeInSamePackage)
125                {
126                    return true;
127                }
128                boolean isInSamePackage = thisPackageName.equals(cursor.packageName);
129
130                if (isInSamePackage)
131                {
132                    return true;
133                }
134            }
135
136            cursor = cursor.parent;
137        }
138
139        return false;
140    }
141
142    /**
143     * Returns true if the class represented by this data, or any parent data, implements
144     * the named interface.
145     */
146    public boolean isInterfaceImplemented(String name)
147    {
148        InheritanceData cursor = this;
149
150        while (cursor != null)
151        {
152            if (cursor.interfaceNames.contains(name))
153            {
154                return true;
155            }
156
157            cursor = cursor.parent;
158        }
159
160        return false;
161    }
162
163    public void addInterface(String name)
164    {
165        if (!interfaceNames.contains(name))
166        {
167            interfaceNames.add(name);
168        }
169    }
170
171    /**
172     * Combines a method name and its desc (which describes parameter types and return value) to form
173     * a value, which is how methods are tracked.
174     */
175    private static String toValue(String name, String desc)
176    {
177        // TAP5-2268: ignore return-type to avoid methods with the same number (and type) of parameters but different
178        //            return-types which is illegal in Java.
179        // desc is something like "(I)Ljava/lang/String;", which means: takes an int, returns a String. We strip
180        // everything after the parameter list.
181        int endOfParameterSpecIdx = desc.indexOf(')');
182
183        return (name + ":" + desc.substring(0, endOfParameterSpecIdx+1)).intern();
184    }
185
186    /**
187     * Returns the names of any methods in this bundle, or from any parent bundles.
188     */
189    public Set<String> methodNames()
190    {
191        Set<String> result = PlasticInternalUtils.newSet();
192
193        InheritanceData cursor = this;
194
195        while (cursor != null)
196        {
197            result.addAll(cursor.methodNames);
198            cursor = cursor.parent;
199        }
200
201        return result;
202    }
203}