/*
* This file is part of LanternServer, licensed under the MIT License (MIT).
*
* Copyright (c) LanternPowered <https://www.lanternpowered.org>
* Copyright (c) SpongePowered <https://www.spongepowered.org>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the Software), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.lanternpowered.server.event.filter;
import static org.objectweb.asm.Opcodes.AASTORE;
import static org.objectweb.asm.Opcodes.ACC_FINAL;
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
import static org.objectweb.asm.Opcodes.ACC_SUPER;
import static org.objectweb.asm.Opcodes.ALOAD;
import static org.objectweb.asm.Opcodes.ANEWARRAY;
import static org.objectweb.asm.Opcodes.ARETURN;
import static org.objectweb.asm.Opcodes.BIPUSH;
import static org.objectweb.asm.Opcodes.DUP;
import static org.objectweb.asm.Opcodes.ICONST_0;
import static org.objectweb.asm.Opcodes.ICONST_1;
import static org.objectweb.asm.Opcodes.ILOAD;
import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
import static org.objectweb.asm.Opcodes.RETURN;
import static org.objectweb.asm.Opcodes.V1_6;
import com.google.common.collect.Lists;
import org.lanternpowered.server.event.filter.delegate.AfterCauseFilterSourceDelegate;
import org.lanternpowered.server.event.filter.delegate.AllCauseFilterSourceDelegate;
import org.lanternpowered.server.event.filter.delegate.BeforeCauseFilterSourceDelegate;
import org.lanternpowered.server.event.filter.delegate.CancellationEventFilterDelegate;
import org.lanternpowered.server.event.filter.delegate.ExcludeSubtypeFilterDelegate;
import org.lanternpowered.server.event.filter.delegate.FilterDelegate;
import org.lanternpowered.server.event.filter.delegate.FirstCauseFilterSourceDelegate;
import org.lanternpowered.server.event.filter.delegate.GetterFilterSourceDelegate;
import org.lanternpowered.server.event.filter.delegate.HasDataFilterDelegate;
import org.lanternpowered.server.event.filter.delegate.IncludeSubtypeFilterDelegate;
import org.lanternpowered.server.event.filter.delegate.LastCauseFilterSourceDelegate;
import org.lanternpowered.server.event.filter.delegate.NamedCauseFilterSourceDelegate;
import org.lanternpowered.server.event.filter.delegate.ParameterFilterDelegate;
import org.lanternpowered.server.event.filter.delegate.ParameterFilterSourceDelegate;
import org.lanternpowered.server.event.filter.delegate.RootCauseFilterSourceDelegate;
import org.lanternpowered.server.event.filter.delegate.SubtypeFilterDelegate;
import org.lanternpowered.server.event.filter.delegate.SupportsDataFilterDelegate;
import org.lanternpowered.server.game.Lantern;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
import org.spongepowered.api.event.Cancellable;
import org.spongepowered.api.event.Event;
import org.spongepowered.api.event.filter.Getter;
import org.spongepowered.api.event.filter.IsCancelled;
import org.spongepowered.api.event.filter.cause.After;
import org.spongepowered.api.event.filter.cause.All;
import org.spongepowered.api.event.filter.cause.Before;
import org.spongepowered.api.event.filter.cause.First;
import org.spongepowered.api.event.filter.cause.Last;
import org.spongepowered.api.event.filter.cause.Named;
import org.spongepowered.api.event.filter.cause.Root;
import org.spongepowered.api.event.filter.data.Has;
import org.spongepowered.api.event.filter.data.Supports;
import org.spongepowered.api.event.filter.type.Exclude;
import org.spongepowered.api.event.filter.type.Include;
import org.spongepowered.api.util.Tristate;
import org.spongepowered.api.util.Tuple;
import org.spongepowered.api.util.generator.event.factory.ClassGenerator;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
public class FilterGenerator {
private static final boolean FILTER_DEBUG = Boolean.parseBoolean(System.getProperty("sponge.filter.debug", "false"));
public static FilterGenerator getInstance() {
return Holder.INSTANCE;
}
private FilterGenerator() {
}
public byte[] generateClass(String name, Method method) {
name = name.replace('.', '/');
Parameter[] params = method.getParameters();
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
MethodVisitor mv;
cw.visit(V1_6, ACC_PUBLIC + ACC_FINAL + ACC_SUPER, name, null, "java/lang/Object", new String[] { Type.getInternalName(EventFilter.class) });
SubtypeFilterDelegate sfilter = null;
List<FilterDelegate> additional = Lists.newArrayList();
boolean cancellation = false;
for (Annotation anno : method.getAnnotations()) {
Object obj = filterFromAnnotation(anno.annotationType());
if (obj == null) {
continue;
}
if (obj instanceof SubtypeFilter) {
if (sfilter != null) {
throw new IllegalStateException("Cannot have both @Include and @Exclude annotations present at once");
}
sfilter = ((SubtypeFilter) obj).getDelegate(anno);
} else if (obj instanceof EventTypeFilter) {
EventTypeFilter etf = (EventTypeFilter) obj;
additional.add(etf.getDelegate(anno));
if (etf == EventTypeFilter.CANCELLATION) {
cancellation = true;
}
}
}
if (!cancellation && Cancellable.class.isAssignableFrom(method.getParameterTypes()[0])) {
additional.add(new CancellationEventFilterDelegate(Tristate.FALSE));
}
if (sfilter != null) {
sfilter.createFields(cw);
}
{
mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
if (sfilter != null) {
sfilter.writeCtor(name, cw, mv);
}
mv.visitInsn(RETURN);
mv.visitMaxs(0, 0);
mv.visitEnd();
}
{
mv = cw.visitMethod(ACC_PUBLIC, "filter", "(" + Type.getDescriptor(Event.class) + ")[Ljava/lang/Object;", null, null);
mv.visitCode();
// index of the next available local variable
int local = 2;
if (sfilter != null) {
local = sfilter.write(name, cw, mv, method, local);
}
for (FilterDelegate eventFilter : additional) {
local = eventFilter.write(name, cw, mv, method, local);
}
// local var indices of the parameters values
int[] plocals = new int[params.length - 1];
for (int i = 1; i < params.length; i++) {
Parameter param = params[i];
ParameterFilterSourceDelegate source = null;
List<ParameterFilterDelegate> paramFilters = Lists.newArrayList();
for (Annotation anno : param.getAnnotations()) {
Object obj = filterFromAnnotation(anno.annotationType());
if (obj == null) {
continue;
}
if (obj instanceof ParameterSource) {
if (source != null) {
throw new IllegalStateException("Cannot have multiple parameter filter source annotations (for " + param.getName() + ")");
}
source = ((ParameterSource) obj).getDelegate(anno);
} else if (obj instanceof ParameterFilter) {
paramFilters.add(((ParameterFilter) obj).getDelegate(anno));
}
}
if (source == null) {
throw new IllegalStateException("Cannot have additional parameters filters without a source (for " + param.getName() + ")");
}
if (source instanceof AllCauseFilterSourceDelegate && !paramFilters.isEmpty()) {
// TODO until better handling for filtering arrays is added
throw new IllegalStateException(
"Cannot have additional parameters filters without an array source (for " + param.getName() + ")");
}
Tuple<Integer, Integer> localState = source.write(cw, mv, method, param, local);
local = localState.getFirst();
plocals[i - 1] = localState.getSecond();
for (ParameterFilterDelegate paramFilter : paramFilters) {
paramFilter.write(cw, mv, method, param, plocals[i - 1]);
}
}
// create the return array
if (params.length == 1) {
mv.visitInsn(ICONST_1);
} else {
mv.visitIntInsn(BIPUSH, params.length);
}
mv.visitTypeInsn(ANEWARRAY, "java/lang/Object");
// load the event into the array
mv.visitInsn(DUP);
mv.visitInsn(ICONST_0);
mv.visitVarInsn(ALOAD, 1);
mv.visitInsn(AASTORE);
// load all the params into the array
for (int i = 1; i < params.length; i++) {
mv.visitInsn(DUP);
mv.visitIntInsn(BIPUSH, i);
Type paramType = Type.getType(params[i].getType());
mv.visitVarInsn(paramType.getOpcode(ILOAD), plocals[i - 1]);
ClassGenerator.visitBoxingMethod(mv, paramType);
mv.visitInsn(AASTORE);
}
mv.visitInsn(ARETURN);
mv.visitMaxs(0, 0);
mv.visitEnd();
}
cw.visitEnd();
final byte[] data = cw.toByteArray();
if (FILTER_DEBUG) {
final Path outDir = Paths.get(".sponge.debug.out");
final Path outFile = outDir.resolve(name + ".class");
if (!Files.exists(outFile.getParent())) {
try {
Files.createDirectories(outFile.getParent());
} catch (IOException e) {
Lantern.getLogger().warn("Unable to create the filter debug directory.", e);
}
}
try (final OutputStream out = Files.newOutputStream(outFile)) {
out.write(data);
} catch (IOException e) {
Lantern.getLogger().warn("Unable to create the filter debug class.", e);
}
}
return data;
}
private static Object filterFromAnnotation(Class<? extends Annotation> cls) {
Object filter;
if ((filter = SubtypeFilter.valueOf(cls)) != null)
return filter;
if ((filter = EventTypeFilter.valueOf(cls)) != null)
return filter;
if ((filter = ParameterSource.valueOf(cls)) != null)
return filter;
if ((filter = ParameterFilter.valueOf(cls)) != null)
return filter;
return null;
}
private enum SubtypeFilter {
INCLUDE(Include.class),
EXCLUDE(Exclude.class),
;
private final Class<? extends Annotation> cls;
SubtypeFilter(Class<? extends Annotation> cls) {
this.cls = cls;
}
public SubtypeFilterDelegate getDelegate(Annotation anno) {
if (this == INCLUDE) {
return new IncludeSubtypeFilterDelegate((Include) anno);
} else if (this == EXCLUDE) {
return new ExcludeSubtypeFilterDelegate((Exclude) anno);
}
throw new UnsupportedOperationException();
}
public static SubtypeFilter valueOf(Class<? extends Annotation> cls) {
for (SubtypeFilter value : values()) {
if (value.cls.equals(cls)) {
return value;
}
}
return null;
}
}
private enum EventTypeFilter {
CANCELLATION(IsCancelled.class),
;
private final Class<? extends Annotation> cls;
EventTypeFilter(Class<? extends Annotation> cls) {
this.cls = cls;
}
public FilterDelegate getDelegate(Annotation anno) {
if (this == CANCELLATION) {
return new CancellationEventFilterDelegate(((IsCancelled) anno).value());
}
throw new UnsupportedOperationException();
}
public static EventTypeFilter valueOf(Class<? extends Annotation> cls) {
for (EventTypeFilter value : values()) {
if (value.cls.equals(cls)) {
return value;
}
}
return null;
}
}
private enum ParameterSource {
CAUSE_FIRST(First.class),
CAUSE_LAST(Last.class),
CAUSE_BEFORE(Before.class),
CAUSE_AFTER(After.class),
CAUSE_ALL(All.class),
CAUSE_ROOT(Root.class),
CAUSE_NAMED(Named.class),
GETTER(Getter.class),
;
private final Class<? extends Annotation> cls;
ParameterSource(Class<? extends Annotation> cls) {
this.cls = cls;
}
public ParameterFilterSourceDelegate getDelegate(Annotation anno) {
if (this == CAUSE_FIRST) {
return new FirstCauseFilterSourceDelegate((First) anno);
}
if (this == CAUSE_LAST) {
return new LastCauseFilterSourceDelegate((Last) anno);
}
if (this == CAUSE_BEFORE) {
return new BeforeCauseFilterSourceDelegate((Before) anno);
}
if (this == CAUSE_AFTER) {
return new AfterCauseFilterSourceDelegate((After) anno);
}
if (this == CAUSE_ALL) {
return new AllCauseFilterSourceDelegate((All) anno);
}
if (this == CAUSE_ROOT) {
return new RootCauseFilterSourceDelegate((Root) anno);
}
if (this == CAUSE_NAMED) {
return new NamedCauseFilterSourceDelegate((Named) anno);
}
if (this == GETTER) {
return new GetterFilterSourceDelegate((Getter) anno);
}
throw new UnsupportedOperationException();
}
public static ParameterSource valueOf(Class<? extends Annotation> cls) {
for (ParameterSource value : values()) {
if (value.cls.equals(cls)) {
return value;
}
}
return null;
}
}
private enum ParameterFilter {
SUPPORTS(Supports.class),
HAS(Has.class),
;
private final Class<? extends Annotation> cls;
ParameterFilter(Class<? extends Annotation> cls) {
this.cls = cls;
}
public ParameterFilterDelegate getDelegate(Annotation anno) {
if (this == SUPPORTS) {
return new SupportsDataFilterDelegate((Supports) anno);
}
if (this == HAS) {
return new HasDataFilterDelegate((Has) anno);
}
throw new UnsupportedOperationException();
}
public static ParameterFilter valueOf(Class<? extends Annotation> cls) {
for (ParameterFilter value : values()) {
if (value.cls.equals(cls)) {
return value;
}
}
return null;
}
}
private static final class Holder {
static final FilterGenerator INSTANCE = new FilterGenerator();
}
}