# P2JQueryStatistics.java

    /*
    ** Module   : P2JQueryStatistics.java
    ** Abstract : Manages statistics for P2J queries
    **
    ** Copyright (c) 2023, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
    ** 001 RAA 20230522 First revision with basic usage.
    ** 002 RAA 20230802 Introduced two maps to store overall statistics.
    **     RAA 20231122 Removed location and fql fields from main class. Refactored gatherQueryDetails to use
    **                  indexed looping. Pre-computed hashCode for both subclasses.
    */

    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 of the
    ** License, or (at your option) any later version.
    **
    ** This program is distributed in the hope that it will be useful,
    ** but WITHOUT ANY WARRANTY; without even the implied warranty of
    ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    ** GNU Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */

    package com.goldencode.p2j.persist;

    import java.util.*;

    /**
     * Class responsible with storing information about P2J Queries.
     * For the moment, only {@link CompoundQuery} and {@link RandomAccessQuery} support this type
     * of storing statistics.
     */
    public class P2JQueryStatistics
    {
       /** Transient map of query instances to information about where they were constructed. */
       private static final Map<Key, P2JQueryStatistics> statistics = new LinkedHashMap<>();

       /** Map that stores the location of queries. */
       private static final Map<P2JQuery, Location> locations = new WeakHashMap<>();

       /** Number of times query was executed at this location (across query instances). */
       private long useCount = 0;

       /** The execution time of this query until this moment. */
       private double time = 0;

       /**
        * Default constructor.
        */
       private P2JQueryStatistics()
       {

       }

       /**
        * Function that retrieves/creates the statistics of a query.
        *
        * @param   query
        *          The query in use.
        * @param   fql
        *          The FQL of the query.
        *
        * @return  P2JQueryStatistics
        *          An instance of {@code P2JQueryStatistics} denoting the statistics of the given query.
        */
       public static P2JQueryStatistics getStatistics(P2JQuery query, String fql)
       {
          // First find/create the location of the query
          Location location = null;
          synchronized (locations)
          {
             location = locations.get(query);
          }

          if (location == null)
          {
             location = gatherQueryDetails();
             synchronized (locations)
             {
                locations.put(query, location);
             }
          }

          // Then access its statistics
          Key key = new Key(location, fql);
          P2JQueryStatistics info = null;
          synchronized (statistics)
          {
             info = statistics.get(key);
          }

          if (info == null)
          {
             info = new P2JQueryStatistics();
             synchronized (statistics)
             {
                statistics.put(key, info);
             }
          }

          return info;
       }

       /**
        * Function that returns the location of a query (if one exists), in a ready to be printed format.
        *
        * @param   query
        *          The given query.
        *
        * @return  String
        *          The formatted string that denotes the location of the query, or {@code null} if no such
        *          location exists.
        */
       public static String getLocationFormattedOutput(P2JQuery query)
       {
          Location location = null;
          synchronized (locations)
          {
             location = locations.get(query);
          }

          return location == null ? null : location.toString();
       }

       /**
        * Get the number of times the FQL query was executed at this location, across all query
        * instances constructed at the same location.
        *
        * @return  long
        *          Query execution count.
        */
       public long getUseCount()
       {
          return useCount;
       }

       /**
        * Increment by one the number of times the FQL query was executed at this location, across
        * all query instances constructed at the same location.
        */
       public void incrementUseCount()
       {
          useCount++;
       }

       /**
        * Getter for the {@code time} field.
        *
        * @return  double
        *          The execution time of the query until this moment.
        */
       public double getTime()
       {
          return time;
       }

       /**
        * Method for increasing the total time of this query by a specified amount.
        *
        * @param   time
        *          The time that will be added to the total time.
        */
       public void increaseTime(double time)
       {
          this.time += time;
       }

       /**
        * Function that navigates the stack trace, identifies details of a query (such as file name, method name,
        * line number) and creates a {@code Location} based on those findings.
        *
        * @return  Location
        *          The location of a query.
        */
       private static Location gatherQueryDetails()
       {
          Throwable t = new Throwable();
          String queryClass = null;
          String fileName = null;
          String methodName = null;
          String className = null;
          int lineNumber = -1;
          String previousFileName = null;
          String previousMethodName = null;
          String previousClassName = null;
          int previousLineNumber = -1;
          StackTraceElement[] stackTraceElements = t.getStackTrace();
          for (int i = 0; i < stackTraceElements.length; i++)
          {
             className = stackTraceElements[i].getClassName();
             fileName = stackTraceElements[i].getFileName();
             lineNumber = stackTraceElements[i].getLineNumber();
             methodName = stackTraceElements[i].getMethodName();

             if (queryClass != null && !fileName.equals(queryClass))
             {
                return new Location(previousFileName, previousLineNumber, previousClassName, previousMethodName);
             }
             else if (queryClass == null && className.lastIndexOf('$') < 0 && !fileName.startsWith("P2JQuery"))
             {
                queryClass = fileName;
             }

             previousClassName = className;
             previousFileName = fileName;
             previousLineNumber = lineNumber;
             previousMethodName = methodName;
          }

          return null;
       }

       /**
        * Helper class which stores location information regarding the construction of a P2J query.
        */
       private static class Location
       {
          /** Unqualified application source code file name containing query. */
          private final String sourceFile;

          /** Line number at which query was constructed. */
          private final int lineNumber;

          /** Name of application class (sans package root name) containing query. */
          private final String className;

          /** Name of application-level method in which query was executed. */
          private final String methodName;

          /** The hash code of this {@code Location}. */
          private final int hashCode;

          /**
           * Constructor.
           *
           * @param   sourceFile
           *          Unqualified application source code file name containing query.
           * @param   lineNumber
           *          Line number at which query was constructed.
           * @param   className
           *          Name of application class (sans package root name) containing query.
           * @param   methodName
           *          Name of application-level method in which query was executed
           */
          private Location(String sourceFile, int lineNumber, String className, String methodName)
          {
             this.sourceFile = sourceFile.intern();
             this.lineNumber = lineNumber;
             this.className = className.intern();
             this.methodName = methodName.intern();

             hashCode =  37 *
                        (37 *
                        (37 *
                        (37 * 17 + lineNumber)
                                 + className.hashCode())
                                 + methodName.hashCode())
                                 + sourceFile.hashCode();
          }

          /**
           * Return the hash code computed for this object instance.
           *
           * @return  int
           *          Hash code.
           */
          @Override
          public int hashCode()
          {
             return hashCode;
          }

          /**
           * Indicate whether this object instance is equivalent to the given instance, based on
           * the content of both.
           *
           * @return  boolean
           *          {@code true} if equivalent, {@code false} otherwise.
           */
          @Override
          public boolean equals(Object o)
          {
             if (!(o instanceof Location))
             {
                return false;
             }

             Location that = (Location) o;

             return this.lineNumber == that.lineNumber &&
                    this.className  == that.className  &&
                    this.methodName == that.methodName &&
                    this.sourceFile == that.sourceFile;
          }

          /**
           * A formatted way of presenting a {@code Location}.
           *
           * @return  String
           *          The {@code String} representing the {@code Location}.
           */
          @Override
          public String toString()
          {
             StringBuilder result = new StringBuilder();
             result.append("Source file: ").append(sourceFile)
                   .append("; Method name: ").append(methodName)
                   .append("; Line number: ").append(lineNumber);
             return result.toString();
          }
       }

       /**
        * A hashable key comprised of both location information and an FQL string. Location info
        * alone is not enough to differentiate queries, because different FQL can be executed
        * from a query constructed at the same code location.
        */
       private static class Key
       {
          /** Query construction location information. */
          private final Location location;

          /** Dialect-specific FQL query string. */
          private final String fql;

          /** The hash code of this {@code Key}. */
          private final int hashCode;

          /**
           * Constructor.
           *
           * @param   location
           *          Query construction location information.
           * @param   fql
           *          Dialect-specific FQL query string.
           */
          Key(Location location, String fql)
          {
             this.location = location;
             this.fql = fql;

             hashCode = 37 * (37 * 17 + location.hashCode()) + fql.hashCode();
          }

          /**
           * Return the hash code computed for this object instance.
           *
           * @return  int
           *          Hash code.
           */
          public int hashCode()
          {
             return hashCode;
          }

          /**
           * Indicate whether this object instance is equivalent to the given instance, based on
           * the content of both.
           *
           * @return  boolean
           *          {@code true} if equivalent, {@code false} otherwise.
           */
          public boolean equals(Object o)
          {
             if (!(o instanceof Key))
             {
                return false;
             }

             Key that = (Key) o;
             return (this.fql.equals(that.fql) && this.location.equals(that.location));
          }
       }
    }