/* * 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.phoenix.expression.function; import static org.apache.phoenix.query.QueryServices.DYNAMIC_JARS_DIR_KEY; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.List; import java.util.concurrent.ConcurrentMap; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; import org.apache.hadoop.hbase.HBaseConfiguration; import org.apache.hadoop.hbase.io.ImmutableBytesWritable; import org.apache.hadoop.hbase.util.DynamicClassLoader; import org.apache.hadoop.io.WritableUtils; import org.apache.phoenix.compile.KeyPart; import org.apache.phoenix.expression.Expression; import org.apache.phoenix.expression.visitor.ExpressionVisitor; import org.apache.phoenix.parse.PFunction; import org.apache.phoenix.schema.PName; import org.apache.phoenix.schema.PNameFactory; import org.apache.phoenix.schema.tuple.Tuple; import org.apache.phoenix.schema.types.PDataType; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.MapMaker; public class UDFExpression extends ScalarFunction { private static Configuration config = HBaseConfiguration.create(); private static final ConcurrentMap<PName, DynamicClassLoader> tenantIdSpecificCls = new MapMaker().concurrencyLevel(3).weakValues().makeMap(); private static final ConcurrentMap<String, DynamicClassLoader> pathSpecificCls = new MapMaker().concurrencyLevel(3).weakValues().makeMap(); private PName tenantId; private String functionClassName; private String jarPath; private ScalarFunction udfFunction; public UDFExpression() { } public UDFExpression(List<Expression> children,PFunction functionInfo) { super(children); this.tenantId = functionInfo.getTenantId() == null ? PName.EMPTY_NAME : functionInfo.getTenantId(); this.functionClassName = functionInfo.getClassName(); this.jarPath = functionInfo.getJarPath(); constructUDFFunction(); } public UDFExpression(List<Expression> children, PName tenantId, String functionClassName, String jarPath, ScalarFunction udfFunction) { super(children); this.tenantId = tenantId; this.functionClassName = functionClassName; this.jarPath = jarPath; if(udfFunction != null) { this.udfFunction = udfFunction; } else { constructUDFFunction(); } } @Override public boolean evaluate(Tuple tuple, ImmutableBytesWritable ptr) { return udfFunction.evaluate(tuple, ptr); } @Override public <T> T accept(ExpressionVisitor<T> visitor) { return udfFunction.accept(visitor); } @Override public PDataType getDataType() { return udfFunction.getDataType(); } @Override public String getName() { return udfFunction.getName(); } @Override public OrderPreserving preservesOrder() { return udfFunction.preservesOrder(); } @Override public KeyPart newKeyPart(KeyPart childPart) { return udfFunction.newKeyPart(childPart); } @Override public int getKeyFormationTraversalIndex() { return udfFunction.getKeyFormationTraversalIndex(); } public PName getTenantId() { return tenantId; } public String getFunctionClassName() { return functionClassName; } public String getJarPath() { return jarPath; } public ScalarFunction getUdfFunction() { return udfFunction; } @Override public void write(DataOutput output) throws IOException { super.write(output); WritableUtils.writeString(output, tenantId.getString()); WritableUtils.writeString(output, this.functionClassName); if(this.jarPath == null) { WritableUtils.writeString(output, ""); } else { WritableUtils.writeString(output, this.jarPath); } } @Override public void readFields(DataInput input) throws IOException { super.readFields(input); this.tenantId = PNameFactory.newName(WritableUtils.readString(input)); this.functionClassName = WritableUtils.readString(input); String str = WritableUtils.readString(input); this.jarPath = str.length() == 0 ? null: str; constructUDFFunction(); } private void constructUDFFunction() { try { DynamicClassLoader classLoader = getClassLoader(this.tenantId, this.jarPath); Class<?> clazz = classLoader.loadClass(this.functionClassName); Constructor<?> constructor = clazz.getConstructor(List.class); udfFunction = (ScalarFunction)constructor.newInstance(this.children); } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new RuntimeException(e); } } public static DynamicClassLoader getClassLoader(final PName tenantId, final String jarPath) { DynamicClassLoader cl = tenantIdSpecificCls.get(tenantId); Path parent = null; if (cl != null) return cl; if(jarPath != null && !jarPath.isEmpty()) { cl = pathSpecificCls.get(jarPath); if (cl != null) return cl; parent = getPathForParent(jarPath); } // Parse the DYNAMIC_JARS_DIR_KEY value as a Path if it's present in the configuration Path allowedDynamicJarsPath = config.get(DYNAMIC_JARS_DIR_KEY) != null ? new Path(config.get(DYNAMIC_JARS_DIR_KEY)) : null; // The case jarPath is not provided, or it is provided and the jar is inside hbase.dynamic.jars.dir if (jarPath == null || jarPath.isEmpty() || (allowedDynamicJarsPath != null && parent != null && parent.equals(allowedDynamicJarsPath))) { cl = tenantIdSpecificCls.get(tenantId); if (cl == null) { cl = new DynamicClassLoader(config, UDFExpression.class.getClassLoader()); } // Cache class loader as a weak value, will be GC'ed when no reference left DynamicClassLoader prev = tenantIdSpecificCls.putIfAbsent(tenantId, cl); if (prev != null) { cl = prev; } return cl; } else { //The case jarPath is provided as not part of DYNAMIC_JARS_DIR_KEY //As per PHOENIX-4231, DYNAMIC_JARS_DIR_KEY is the only place where loading a udf jar is allowed throw new SecurityException("Loading jars from " + jarPath + " is not allowed. The only location that is allowed is "+ config.get(DYNAMIC_JARS_DIR_KEY)); } } public static Path getPathForParent(String jarPath) { Path path = new Path(jarPath); if (jarPath.endsWith(".jar")) { return path.getParent(); } return path; } @VisibleForTesting public static void setConfig(Configuration conf) { config = conf; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (!(obj instanceof UDFExpression)) { return false; } UDFExpression that = (UDFExpression) obj; if (!this.udfFunction.getName().equals(that.udfFunction.getName())) { return false; } if (!this.udfFunction.getChildren().equals( that.udfFunction.getChildren())) { return false; } if (!functionClassName.equals(that.functionClassName)) { return false; } if (!jarPath.equals(that.jarPath)) { return false; } return true; } }