LocationUtils.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.struts2.util.location;

import org.apache.struts2.util.ClassLoaderUtil;
import org.apache.struts2.config.StrutsJavaConfiguration;
import org.w3c.dom.Element;
import org.xml.sax.Locator;
import org.xml.sax.SAXParseException;

import javax.xml.transform.SourceLocator;
import javax.xml.transform.TransformerException;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

/**
 * Location-related utility methods.
 */
public class LocationUtils {

    /**
     * The string representation of an unknown location: "<code>[unknown location]</code>".
     */
    public static final String UNKNOWN_STRING = "[unknown location]";

    private static List<WeakReference<LocationFinder>> finders = new ArrayList<>();

    /**
     * An finder or object locations
     */
    public interface LocationFinder {
        /**
         * Get the location of an object
         * @param obj the object for which to find a location
         * @param description and optional description to be added to the object's location
         * @return the object's location or <code>null</code> if object's class isn't handled
         *         by this finder.
         */
        Location getLocation(Object obj, String description);
    }

    private LocationUtils() {
        // Forbid instanciation
    }

    /**
     * Builds a string representation of a location, in the
     * "<code><em>descripton</em> - <em>uri</em>:<em>line</em>:<em>column</em></code>"
     * format (e.g. "<code>foo - file://path/to/file.xml:3:40</code>"). For {@link Location#UNKNOWN an unknown location}, returns
     * {@link #UNKNOWN_STRING}.
     *
     * @param location location object
     *
     * @return the string representation
     */
    public static String toString(Location location) {
        StringBuilder result = new StringBuilder();

        String description = location.getDescription();
        if (description != null) {
            result.append(description).append(" - ");
        }

        String uri = location.getURI();
        if (uri != null) {
            result.append(uri).append(':').append(location.getLineNumber()).append(':').append(location.getColumnNumber());
        } else {
            result.append(UNKNOWN_STRING);
        }

        return result.toString();
    }

    /**
     * Parse a location string of the form "<code><em>uri</em>:<em>line</em>:<em>column</em></code>" (e.g.
     * "<code>path/to/file.xml:3:40</code>") to a Location object. Additionally, a description may
     * also optionally be present, separated with an hyphen (e.g. "<code>foo - path/to/file.xml:3.40</code>").
     *
     * @param text the text to parse
     * @return the location (possibly <code>null</code> if text was null or in an incorrect format)
     */
    public static LocationImpl parse(String text) throws IllegalArgumentException {
        if (text == null || text.isEmpty()) {
            return null;
        }

        // Do we have a description?
        String description;
        int uriStart = text.lastIndexOf(" - "); // lastIndexOf to allow the separator to be in the description
        if (uriStart > -1) {
            description = text.substring(0, uriStart);
            uriStart += 3; // strip " - "
        } else {
            description = null;
            uriStart = 0;
        }

        try {
            int colSep = text.lastIndexOf(':');
            if (colSep > -1) {
                int column = Integer.parseInt(text.substring(colSep + 1));

                int lineSep = text.lastIndexOf(':', colSep - 1);
                if (lineSep > -1) {
                    int line = Integer.parseInt(text.substring(lineSep + 1, colSep));
                    return new LocationImpl(description, text.substring(uriStart, lineSep), line, column);
                }
            } else {
                // unkonwn?
                if (text.endsWith(UNKNOWN_STRING)) {
                    return LocationImpl.UNKNOWN;
                }
            }
        } catch(Exception e) {
            // Ignore: handled below
        }

        return LocationImpl.UNKNOWN;
    }

    /**
     * Checks if a location is known, i.e. it is not null nor equal to {@link Location#UNKNOWN}.
     *
     * @param location the location to check
     * @return <code>true</code> if the location is known
     */
    public static boolean isKnown(Location location) {
        return location != null && !Location.UNKNOWN.equals(location);
    }

    /**
     * Checks if a location is unknown, i.e. it is either null or equal to {@link Location#UNKNOWN}.
     *
     * @param location the location to check
     * @return <code>true</code> if the location is unknown
     */
    public static boolean isUnknown(Location location) {
        return location == null || Location.UNKNOWN.equals(location);
    }

    /**
     * Add a {@link LocationFinder} to the list of finders that will be queried for an object's
     * location by {@link #getLocation(Object, String)}.
     * <p>
     * <b>Important:</b> LocationUtils internally stores a weak reference to the finder. This
     * avoids creating strong links between the classloader holding this class and the finder's
     * classloader, which can cause some weird memory leaks if the finder's classloader is to
     * be reloaded. Therefore, you <em>have</em> to keep a strong reference to the finder in the
     * calling code, e.g.:
     * <pre>
     *   private static LocationUtils.LocationFinder myFinder =
     *       new LocationUtils.LocationFinder() {
     *           public Location getLocation(Object obj, String desc) {
     *               ...
     *           }
     *       };
     *
     *   static {
     *       LocationUtils.addFinder(myFinder);
     *   }
     * </pre>
     *
     * @param finder the location finder to add
     */
    public static void addFinder(LocationFinder finder) {
        if (finder == null) {
            return;
        }

        synchronized(LocationFinder.class) {
            // Update a clone of the current finder list to avoid breaking
            // any iteration occuring in another thread.
            List<WeakReference<LocationFinder>> newFinders = new ArrayList<>(finders);
            newFinders.add(new WeakReference<>(finder));
            finders = newFinders;
        }
    }

    /**
     * Get the location of an object. Some well-known located classes built in the JDK are handled
     * by this method. Handling of other located classes can be handled by adding new location finders.
     *
     * @param obj the object of which to get the location
     * @return the object's location, or {@link Location#UNKNOWN} if no location could be found
     */
    public static Location getLocation(Object obj) {
        return getLocation(obj, null);
    }

    /**
     * Get the location of an object. Some well-known located classes built in the JDK are handled
     * by this method. Handling of other located classes can be handled by adding new location finders.
     *
     * @param obj the object of which to get the location
     * @param description an optional description of the object's location, used if a Location object
     *        has to be created.
     * @return the object's location, or {@link Location#UNKNOWN} if no location could be found
     */
    public static Location getLocation(Object obj, String description) {
        if (obj instanceof Location) {
            return (Location) obj;
        }

        if (obj instanceof Locatable) {
            return ((Locatable)obj).getLocation();
        }

        // Check some well-known locatable exceptions
        if (obj instanceof SAXParseException spe) {
            if (spe.getSystemId() != null) {
                return new LocationImpl(description, spe.getSystemId(), spe.getLineNumber(), spe.getColumnNumber());
            } else {
                return Location.UNKNOWN;
            }
        }

        if (obj instanceof TransformerException ex) {
            SourceLocator locator = ex.getLocator();
            if (locator != null && locator.getSystemId() != null) {
                return new LocationImpl(description, locator.getSystemId(), locator.getLineNumber(), locator.getColumnNumber());
            } else {
                return Location.UNKNOWN;
            }
        }

        if (obj instanceof Locator locator) {
            if (locator.getSystemId() != null) {
                return new LocationImpl(description, locator.getSystemId(), locator.getLineNumber(), locator.getColumnNumber());
            } else {
                return Location.UNKNOWN;
            }
        }

        if (obj instanceof Element) {
            return LocationAttributes.getLocation((Element)obj);
        }

        List<WeakReference<LocationFinder>> currentFinders = finders; // Keep the current list
        for (WeakReference<LocationFinder> ref : currentFinders) {
            LocationFinder finder = ref.get();
            if (finder == null) {
                // This finder was garbage collected: update finders
                synchronized (LocationFinder.class) {
                    // Update a clone of the current list to avoid breaking current iterations
                    List<WeakReference<LocationFinder>> newFinders = new ArrayList<>(finders);
                    newFinders.remove(ref);
                    finders = newFinders;
                }
            } else {
                Location result = finder.getLocation(obj, description);
                if (result != null) {
                    return result;
                }
            }
        }

        if (obj instanceof Throwable t) {
            StackTraceElement[] stack = t.getStackTrace();
            if (stack != null && stack.length > 0) {
                StackTraceElement trace = stack[0];
                if (trace.getLineNumber() >= 0) {
                    String uri = trace.getClassName();
                    if (trace.getFileName() != null) {
                        uri = uri.replace('.', '/');
                        uri = uri.substring(0, uri.lastIndexOf('/') + 1);
                        uri = uri + trace.getFileName();
                        URL url = ClassLoaderUtil.getResource(uri, LocationUtils.class);
                        if (url != null) {
                            uri = url.toString();
                        }
                    }
                    if (description == null) {
                        description = "Class: " + trace.getClassName() + "\n" +
                                "File: " + trace.getFileName() + "\n" +
                                "Method: " + trace.getMethodName() + "\n" +
                                "Line: " + trace.getLineNumber();
                    }
                    return new LocationImpl(description, uri, trace.getLineNumber(), -1);
                }
            }
        }

        if (obj instanceof StrutsJavaConfiguration) {
            return new LocationImpl(description, obj.toString());
        }

        return Location.UNKNOWN;
    }
}