/* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch 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.elasticsearch.painless; import org.elasticsearch.script.ScriptException; import java.util.ArrayList; import java.util.BitSet; import java.util.List; import java.util.Map; /** * Abstract superclass on top of which all Painless scripts are built. */ public abstract class PainlessScript { /** * Name of the script set at compile time. */ private final String name; /** * Source of the script. */ private final String source; /** * Character number of the start of each statement. */ private final BitSet statements; protected PainlessScript(String name, String source, BitSet statements) { this.name = name; this.source = source; this.statements = statements; } /** * Adds stack trace and other useful information to exceptions thrown * from a Painless script. * @param t The throwable to build an exception around. * @return The generated ScriptException. */ protected final ScriptException convertToScriptException(Throwable t, Map<String, List<String>> extraMetadata) { // create a script stack: this is just the script portion List<String> scriptStack = new ArrayList<>(); for (StackTraceElement element : t.getStackTrace()) { if (WriterConstants.CLASS_NAME.equals(element.getClassName())) { // found the script portion int offset = element.getLineNumber(); if (offset == -1) { scriptStack.add("<<< unknown portion of script >>>"); } else { offset--; // offset is 1 based, line numbers must be! int startOffset = getPreviousStatement(offset); if (startOffset == -1) { assert false; // should never happen unless we hit exc in ctor prologue... startOffset = 0; } int endOffset = getNextStatement(startOffset); if (endOffset == -1) { endOffset = source.length(); } // TODO: if this is still too long, truncate and use ellipses String snippet = source.substring(startOffset, endOffset); scriptStack.add(snippet); StringBuilder pointer = new StringBuilder(); for (int i = startOffset; i < offset; i++) { pointer.append(' '); } pointer.append("^---- HERE"); scriptStack.add(pointer.toString()); } break; // but filter our own internal stacks (e.g. indy bootstrap) } else if (!shouldFilter(element)) { scriptStack.add(element.toString()); } } // build a name for the script: final String name; if (PainlessScriptEngine.INLINE_NAME.equals(this.name)) { name = source; } else { name = this.name; } ScriptException scriptException = new ScriptException("runtime error", t, scriptStack, name, PainlessScriptEngine.NAME); for (Map.Entry<String, List<String>> entry : extraMetadata.entrySet()) { scriptException.addMetadata(entry.getKey(), entry.getValue()); } return scriptException; } /** returns true for methods that are part of the runtime */ private static boolean shouldFilter(StackTraceElement element) { return element.getClassName().startsWith("org.elasticsearch.painless.") || element.getClassName().startsWith("java.lang.invoke.") || element.getClassName().startsWith("sun.invoke."); } /** * Finds the start of the first statement boundary that is on or before {@code offset}. If one is not found, {@code -1} is returned. */ private int getPreviousStatement(int offset) { return statements.previousSetBit(offset); } /** * Finds the start of the first statement boundary that is after {@code offset}. If one is not found, {@code -1} is returned. */ private int getNextStatement(int offset) { return statements.nextSetBit(offset + 1); } }