    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;
    014
    015import org.apache.tapestry5.*;
    016import org.apache.tapestry5.func.Mapper;
    017import org.apache.tapestry5.ioc.Messages;
    018import org.apache.tapestry5.ioc.Orderable;
    019import org.apache.tapestry5.ioc.Resource;
    020import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
    021import org.apache.tapestry5.ioc.internal.util.InternalUtils;
    022import org.apache.tapestry5.services.ComponentEventRequestParameters;
    023import org.apache.tapestry5.services.LinkCreationListener;
    024import org.apache.tapestry5.services.LinkCreationListener2;
    025import org.apache.tapestry5.services.PageRenderRequestParameters;
    026import org.apache.tapestry5.services.javascript.StylesheetLink;
    027
    028import java.io.IOException;
    029import java.io.InputStream;
    030import java.io.OutputStream;
    031import java.lang.annotation.Annotation;
    032import java.lang.ref.Reference;
    033import java.lang.reflect.Type;
    034import java.util.Arrays;
    035import java.util.List;
    036import java.util.Map;
    037import java.util.regex.Pattern;
    038
    039/**
    040 * Shared utility methods used by various implementation classes.
    041 */
    042@SuppressWarnings("all")
    043public class TapestryInternalUtils
    044{
    045    private static final String SLASH = "/";
    046
    047    private static final Pattern SLASH_PATTERN = Pattern.compile(SLASH);
    048
    049    private static final Pattern NON_WORD_PATTERN = Pattern.compile("[^\\w]");
    050
    051    private static final Pattern COMMA_PATTERN = Pattern.compile("\\s*,\\s*");
    052
    053    private static final int BUFFER_SIZE = 5000;
    054
    055    /**
    056     * Capitalizes the string, and inserts a space before each upper case character (or sequence of upper case
    057     * characters). Thus "userId" becomes "User Id", etc. Also, converts underscore into space (and capitalizes the
    058     * following word), thus "user_id" also becomes "User Id".
    059     */
    060    public static String toUserPresentable(String id)
    061    {
    062        return InternalUtils.toUserPresentable(id);
    063    }
    064
    065    public static Map<String, String> mapFromKeysAndValues(String... keysAndValues)
    066    {
    067        Map<String, String> result = CollectionFactory.newMap();
    068
    069        int i = 0;
    070        while (i < keysAndValues.length)
    071        {
    072            String key = keysAndValues[i++];
    073            String value = keysAndValues[i++];
    074
    075            result.put(key, value);
    076        }
    077
    078        return result;
    079    }
    080
    081    /**
    082     * Converts a string to an {@link OptionModel}. The string is of the form "value=label". If the equals sign is
    083     * omitted, then the same value is used for both value and label.
    084     */
    085    public static OptionModel toOptionModel(String input)
    086    {
    087        assert input != null;
    088        int equalsx = input.indexOf('=');
    089
    090        if (equalsx < 0)
    091            return new OptionModelImpl(input);
    092
    093        String value = input.substring(0, equalsx);
    094        String label = input.substring(equalsx + 1);
    095
    096        return new OptionModelImpl(label, value);
    097    }
    098
    099    /**
    100     * Parses a string input into a series of value=label pairs compatible with {@link #toOptionModel(String)}. Splits
    101     * on commas. Ignores whitespace around commas.
    102     *
    103     * @param input comma seperated list of terms
    104     * @return list of option models
    105     */
    106    public static List<OptionModel> toOptionModels(String input)
    107    {
    108        assert input != null;
    109        List<OptionModel> result = CollectionFactory.newList();
    110
    111        for (String term : input.split(","))
    112            result.add(toOptionModel(term.trim()));
    113
    114        return result;
    115    }
    116
    117    /**
    118     * Wraps the result of {@link #toOptionModels(String)} as a {@link SelectModel} (with no option groups).
    119     *
    120     * See TAP5-2184 for why this ends up causing some trouble!
    121     */
    122    public static SelectModel toSelectModel(String input)
    123    {
    124        List<OptionModel> options = toOptionModels(input);
    125
    126        return new SelectModelImpl(null, options);
    127    }
    128
    129    /**
    130     * Converts a map entry to an {@link OptionModel}.
    131     */
    132    public static OptionModel toOptionModel(Map.Entry input)
    133    {
    134        assert input != null;
    135        String label = input.getValue() != null ? String.valueOf(input.getValue()) : "";
    136
    137        return new OptionModelImpl(label, input.getKey());
    138    }
    139
    140    /**
    141     * Processes a map input into a series of map entries compatible with {@link #toOptionModel(Map.Entry)}.
    142     *
    143     * @param input map of elements
    144     * @return list of option models
    145     */
    146    public static <K, V> List<OptionModel> toOptionModels(Map<K, V> input)
    147    {
    148        assert input != null;
    149        List<OptionModel> result = CollectionFactory.newList();
    150
    151        for (Map.Entry entry : input.entrySet())
    152            result.add(toOptionModel(entry));
    153
    154        return result;
    155    }
    156
    157    /**
    158     * Wraps the result of {@link #toOptionModels(Map)} as a {@link SelectModel} (with no option groups).
    159     */
    160    public static <K, V> SelectModel toSelectModel(Map<K, V> input)
    161    {
    162        List<OptionModel> options = toOptionModels(input);
    163
    164        return new SelectModelImpl(null, options);
    165    }
    166
    167    /**
    168     * Converts an object to an {@link OptionModel}.
    169     */
    170    public static OptionModel toOptionModel(Object input)
    171    {
    172        String label = (input != null ? String.valueOf(input) : "");
    173
    174        return new OptionModelImpl(label, input);
    175    }
    176
    177    /**
    178     * Processes a list input into a series of objects compatible with {@link #toOptionModel(Object)}.
    179     *
    180     * @param input list of elements
    181     * @return list of option models
    182     */
    183    public static <E> List<OptionModel> toOptionModels(List<E> input)
    184    {
    185        assert input != null;
    186        List<OptionModel> result = CollectionFactory.newList();
    187
    188        for (E element : input)
    189            result.add(toOptionModel(element));
    190
    191        return result;
    192    }
    193
    194    /**
    195     * Wraps the result of {@link #toOptionModels(List)} as a {@link SelectModel} (with no option groups).
    196     */
    197    public static <E> SelectModel toSelectModel(List<E> input)
    198    {
    199        List<OptionModel> options = toOptionModels(input);
    200
    201        return new SelectModelImpl(null, options);
    202    }
    203
    204    /**
    205     * Parses a key/value pair where the key and the value are seperated by an equals sign. The key and value are
    206     * trimmed of leading and trailing whitespace, and returned as a {@link KeyValue}.
    207     */
    208    public static KeyValue parseKeyValue(String input)
    209    {
    210        int pos = input.indexOf('=');
    211
    212        if (pos < 1)
    213            throw new IllegalArgumentException(String.format("Key/value pair '%s' is not properly formatted (it does not contain an equals sign).", input));
    214
    215        String key = input.substring(0, pos);
    216        String value = input.substring(pos + 1);
    217
    218        return new KeyValue(key.trim(), value.trim());
    219    }
    220
    221    /**
    222     * Used to convert a property expression into a key that can be used to locate various resources (Blocks, messages,
    223     * etc.). Strips out any punctuation characters, leaving just words characters (letters, number and the
    224     * underscore).
    225     *
    226     * @param expression a property expression
    227     * @return the expression with punctuation removed
    228     */
    229    public static String extractIdFromPropertyExpression(String expression)
    230    {
    231        return InternalUtils.extractIdFromPropertyExpression(expression);
    232    }
    233
    234    /**
    235     * Looks for a label within the messages based on the id. If found, it is used, otherwise the name is converted to a
    236     * user presentable form.
    237     */
    238    public static String defaultLabel(String id, Messages messages, String propertyExpression)
    239    {
    240        return InternalUtils.defaultLabel(id, messages, propertyExpression);
    241    }
    242
    243    /**
    244     * Strips a dotted sequence (such as a property expression, or a qualified class name) down to the last term of that
    245     * expression, by locating the last period ('.') in the string.
    246     */
    247    public static String lastTerm(String input)
    248    {
    249        int dotx = input.lastIndexOf('.');
    250
    251        return input.substring(dotx + 1);
    252    }
    253
    254    /**
    255     * Converts an list of strings into a space-separated string combining them all, suitable for use as an HTML class
    256     * attribute value.
    257     *
    258     * @param classes classes to combine
    259     * @return the joined classes, or null if classes is empty
    260     */
    261    public static String toClassAttributeValue(List<String> classes)
    262    {
    263        if (classes.isEmpty())
    264            return null;
    265
    266        return InternalUtils.join(classes, " ");
    267    }
    268
    269    /**
    270     * Converts an enum to a label string, allowing for overrides from a message catalog.
    271     *
    272     * <ul>
    273     * <li>As key <em>prefix</em>.<em>name</em> if present. Ex: "ElementType.LOCAL_VARIABLE"
    274     * <li>As key <em>name</em> if present, i.e., "LOCAL_VARIABLE".
    275     * <li>As a user-presentable version of the name, i.e., "Local Variable".
    276     * </ul>
    277     *
    278     * @param messages the messages to search for the label
    279     * @param prefix   prepended to key
    280     * @param value    to get a label for
    281     * @return the label
    282     */
    283    public static String getLabelForEnum(Messages messages, String prefix, Enum value)
    284    {
    285        String name = value.name();
    286
    287        String key = prefix + "." + name;
    288
    289        if (messages.contains(key))
    290            return messages.get(key);
    291
    292        if (messages.contains(name))
    293            return messages.get(name);
    294
    295        return toUserPresentable(name.toLowerCase());
    296    }
    297
    298    public static String getLabelForEnum(Messages messages, Enum value)
    299    {
    300        String prefix = lastTerm(value.getClass().getName());
    301
    302        return getLabelForEnum(messages, prefix, value);
    303    }
    304
    305    private static String replace(String input, Pattern pattern, String replacement)
    306    {
    307        return InternalUtils.replace(input, pattern, replacement);
    308    }
    309
    310    /**
    311     * Determines if the two values are equal. They are equal if they are the exact same value (including if they are
    312     * both null). Otherwise standard equals() comparison is used.
    313     *
    314     * @param left  value to compare, possibly null
    315     * @param right value to compare, possibly null
    316     * @return true if same value, both null, or equal
    317     */
    318    public static <T> boolean isEqual(T left, T right)
    319    {
    320        if (left == right)
    321            return true;
    322
    323        if (left == null)
    324            return false;
    325
    326        return left.equals(right);
    327    }
    328
    329    /**
    330     * Splits a path at each slash.
    331     */
    332    public static String[] splitPath(String path)
    333    {
    334        return SLASH_PATTERN.split(path);
    335    }
    336
    337    /**
    338     * Splits a value around commas. Whitespace around the commas is removed, as is leading and trailing whitespace.
    339     *
    340     * @since 5.1.0.0
    341     */
    342    public static String[] splitAtCommas(String value)
    343    {
    344        if (InternalUtils.isBlank(value))
    345            return InternalConstants.EMPTY_STRING_ARRAY;
    346
    347        return COMMA_PATTERN.split(value.trim());
    348    }
    349
    350    /**
    351     * Copies some content from an input stream to an output stream. It is the caller's responsibility to close the
    352     * streams.
    353     *
    354     * @param in  source of data
    355     * @param out sink of data
    356     * @throws IOException
    357     * @since 5.1.0.0
    358     */
    359    public static void copy(InputStream in, OutputStream out) throws IOException
    360    {
    361        byte[] buffer = new byte[BUFFER_SIZE];
    362
    363        while (true)
    364        {
    365            int length = in.read(buffer);
    366
    367            if (length < 0)
    368                break;
    369
    370            out.write(buffer, 0, length);
    371        }
    372
    373        // TAPESTRY-2415: WebLogic needs this flush() call.
    374        out.flush();
    375    }
    376
    377    public static boolean isEqual(EventContext left, EventContext right)
    378    {
    379        if (left == right)
    380            return true;
    381
    382        int count = left.getCount();
    383
    384        if (count != right.getCount())
    385            return false;
    386
    387        for (int i = 0; i < count; i++)
    388        {
    389            if (!left.get(Object.class, i).equals(right.get(Object.class, i)))
    390                return false;
    391        }
    392
    393        return true;
    394    }
    395
    396    /**
    397     * Converts an Asset to an Asset2 if necessary. When actually wrapping an Asset as an Asset2, the asset is assumed
    398     * to be variant (i.e., not cacheable).
    399     *
    400     * @since 5.1.0.0
    401     */
    402    public static Asset2 toAsset2(final Asset asset)
    403    {
    404        if (asset instanceof Asset2)
    405            return (Asset2) asset;
    406
    407        return new Asset2()
    408        {
    409            /** Returns false. */
    410            public boolean isInvariant()
    411            {
    412                return false;
    413            }
    414
    415            public Resource getResource()
    416            {
    417                return asset.getResource();
    418            }
    419
    420            public String toClientURL()
    421            {
    422                return asset.toClientURL();
    423            }
    424
    425            @Override
    426            public String toString()
    427            {
    428                return asset.toString();
    429            }
    430        };
    431    }
    432
    433    public static InternalPropertyConduit toInternalPropertyConduit(final PropertyConduit conduit)
    434    {
    435        if (conduit instanceof InternalPropertyConduit)
    436            return (InternalPropertyConduit) conduit;
    437
    438        return new InternalPropertyConduit()
    439        {
    440
    441            public <T extends Annotation> T getAnnotation(Class<T> annotationClass)
    442            {
    443                return conduit.getAnnotation(annotationClass);
    444            }
    445
    446            public void set(Object instance, Object value)
    447            {
    448                conduit.set(instance, value);
    449            }
    450
    451            public Class getPropertyType()
    452            {
    453                return conduit.getPropertyType();
    454            }
    455
    456            public Type getPropertyGenericType()
    457            {
    458                if (conduit instanceof PropertyConduit2)
    459                {
    460                    return ((PropertyConduit2) conduit).getPropertyGenericType();
    461                }
    462                return conduit.getPropertyType();
    463            }
    464
    465            public Object get(Object instance)
    466            {
    467                return conduit.get(instance);
    468            }
    469
    470            public String getPropertyName()
    471            {
    472                return null;
    473            }
    474        };
    475    }
    476
    477    /**
    478     * @param mixinDef the original mixin definition.
    479     * @return an Orderable whose id is the mixin name.
    480     */
    481    public static Orderable<String> mixinTypeAndOrder(String mixinDef)
    482    {
    483        int idx = mixinDef.indexOf("::");
    484        if (idx == -1)
    485        {
    486            return new Orderable<String>(mixinDef, mixinDef);
    487        }
    488        String type = mixinDef.substring(0, idx);
    489        String[] constraints = splitMixinConstraints(mixinDef.substring(idx + 2));
    490
    491        return new Orderable<String>(type, type, constraints);
    492    }
    493
    494    public static String[] splitMixinConstraints(String s)
    495    {
    496        return InternalUtils.isBlank(s) ? null : s.split(";");
    497    }
    498
    499    /**
    500     * Common mapper, used primarily with {@link org.apache.tapestry5.func.Flow#map(org.apache.tapestry5.func.Mapper)}
    501     *
    502     * @since 5.2.0
    503     */
    504    public static Mapper<Asset, StylesheetLink> assetToStylesheetLink = new Mapper<Asset, StylesheetLink>()
    505    {
    506        public StylesheetLink map(Asset input)
    507        {
    508            return new StylesheetLink(input);
    509        }
    510    };
    511
    512    public static LinkCreationListener2 toLinkCreationListener2(final LinkCreationListener delegate)
    513    {
    514        return new LinkCreationListener2()
    515        {
    516
    517            public void createdPageRenderLink(Link link, PageRenderRequestParameters parameters)
    518            {
    519                delegate.createdPageRenderLink(link);
    520            }
    521
    522            public void createdComponentEventLink(Link link, ComponentEventRequestParameters parameters)
    523            {
    524                delegate.createdComponentEventLink(link);
    525            }
    526        };
    527    }
    528
    529    /**
    530     * @since 5.3
    531     */
    532    public static String toFileSuffix(String fileName)
    533    {
    534        int dotx = fileName.lastIndexOf('.');
    535
    536        return dotx < 0 ? "" : fileName.substring(dotx + 1);
    537    }
    538
    539    /**
    540     * Extracts a value from a  map of references. Handles the case where the reference does not exist,
    541     * and the case where the reference itself now contains null.
    542     *
    543     * @since 5.3
    544     */
    545    public static <K, V> V getAndDeref(Map<K, ? extends Reference<V>> map, K key)
    546    {
    547        Reference<V> ref = map.get(key);
    548
    549        return ref == null ? null : ref.get();
    550    }
    551
    552    /**
    553     * Gathers together an array containing all the threads.
    554     * @since 5.4 */
    555    public static Thread[] getAllThreads() {
    556        ThreadGroup rootGroup = Thread.currentThread().getThreadGroup();
    557
    558        while (true) {
    559            ThreadGroup parentGroup = rootGroup.getParent();
    560            if (parentGroup == null) {
    561                break;
    562            }
    563            rootGroup = parentGroup;
    564        }
    565
    566        Thread[] threads = new Thread[rootGroup.activeCount()];
    567
    568        while (true) {
    569            // A really ugly API. threads.length must be larger than
    570            // the actual number of threads, just so we can determine
    571            // if we're done.
    572            int count = rootGroup.enumerate(threads, true);
    573            if (count < threads.length) {
    574                return Arrays.copyOf(threads, count);
    575            }
    576            threads = new Thread[threads.length * 2];
    577        }
    578    }
    579}
    580