/* * This file is part of Mixin, licensed under the MIT License (MIT). * * 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.spongepowered.asm.mixin.injection; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import org.apache.logging.log4j.LogManager; import org.spongepowered.asm.lib.tree.AnnotationNode; import org.spongepowered.asm.lib.tree.MethodNode; import org.spongepowered.asm.mixin.injection.struct.InjectionInfo; import org.spongepowered.asm.mixin.injection.throwables.InjectionValidationException; import org.spongepowered.asm.util.Annotations; /** * Information store for injector groups */ public class InjectorGroupInfo { /** * Storage for injector groups */ public static final class Map extends HashMap<String, InjectorGroupInfo> { private static final long serialVersionUID = 1L; private static final InjectorGroupInfo NO_GROUP = new InjectorGroupInfo("NONE", true); @Override public InjectorGroupInfo get(Object key) { return this.forName(key.toString()); } /** * Get group for the specified name, creates the group in this map if * it does not already exist * * @param name Name of group to fetch * @return Existing group or new group if none was previously declared */ public InjectorGroupInfo forName(String name) { InjectorGroupInfo value = super.get(name); if (value == null) { value = new InjectorGroupInfo(name); this.put(name, value); } return value; } /** * Parse a group from the specified method, use the default group name * if no group name is specified on the annotation * * @param method (Possibly) annotated method * @param defaultGroup Default group name to use * @return Group or NO_GROUP if no group */ public InjectorGroupInfo parseGroup(MethodNode method, String defaultGroup) { return this.parseGroup(Annotations.getInvisible(method, Group.class), defaultGroup); } /** * Parse a group from the specified annotation, use the default group * name if no group name is specified on the annotation * * @param annotation Annotation or null * @param defaultGroup Default group name to use * @return Group or NO_GROUP if no group */ public InjectorGroupInfo parseGroup(AnnotationNode annotation, String defaultGroup) { if (annotation == null) { return InjectorGroupInfo.Map.NO_GROUP; } String name = Annotations.<String>getValue(annotation, "name"); if (name == null || name.isEmpty()) { name = defaultGroup; } InjectorGroupInfo groupInfo = this.forName(name); Integer min = Annotations.<Integer>getValue(annotation, "min"); if (min != null && min.intValue() != -1) { groupInfo.setMinRequired(min.intValue()); } Integer max = Annotations.<Integer>getValue(annotation, "max"); if (max != null && max.intValue() != -1) { groupInfo.setMaxAllowed(max.intValue()); } return groupInfo; } /** * Validate all groups in this collection * * @throws InjectionValidationException if validation fails */ public void validateAll() throws InjectionValidationException { for (InjectorGroupInfo group : this.values()) { group.validate(); } } } /** * Group name */ private final String name; /** * Members of this group */ private final List<InjectionInfo> members = new ArrayList<InjectionInfo>(); /** * True if this is the default group */ private final boolean isDefault; /** * Number of callbacks we require injected across this group */ private int minCallbackCount = -1; /** * Maximum number of callbacks allowed across this group */ private int maxCallbackCount = Integer.MAX_VALUE; public InjectorGroupInfo(String name) { this(name, false); } InjectorGroupInfo(String name, boolean flag) { this.name = name; this.isDefault = flag; } @Override public String toString() { return String.format("@Group(name=%s, min=%d, max=%d)", this.getName(), this.getMinRequired(), this.getMaxAllowed()); } public boolean isDefault() { return this.isDefault; } public String getName() { return this.name; } public int getMinRequired() { return Math.max(this.minCallbackCount, 1); } public int getMaxAllowed() { return Math.min(this.maxCallbackCount, Integer.MAX_VALUE); } /** * Get all members of this group as a read-only collection * * @return read-only view of group members */ public Collection<InjectionInfo> getMembers() { return Collections.unmodifiableCollection(this.members); } /** * Set the required minimum value for this group. Since this is normally * done on the first {@link Group} annotation it is considered a * warning-level event if a later annotation sets a different value. The * highest value specified on all annotations is always used. * * @param min new value for min required */ public void setMinRequired(int min) { if (min < 1) { throw new IllegalArgumentException("Cannot set zero or negative value for injector group min count. Attempted to set min=" + min + " on " + this); } if (this.minCallbackCount > 0 && this.minCallbackCount != min) { LogManager.getLogger("mixin").warn("Conflicting min value '{}' on @Group({}), previously specified {}", min, this.name, this.minCallbackCount); } this.minCallbackCount = Math.max(this.minCallbackCount, min); } /** * Set the required minimum value for this group. Since this is normally * done on the first {@link Group} annotation it is considered a * warning-level event if a later annotation sets a different value. The * highest value specified on all annotations is always used. * * @param max new value for max allowed */ public void setMaxAllowed(int max) { if (max < 1) { throw new IllegalArgumentException("Cannot set zero or negative value for injector group max count. Attempted to set max=" + max + " on " + this); } if (this.maxCallbackCount < Integer.MAX_VALUE && this.maxCallbackCount != max) { LogManager.getLogger("mixin").warn("Conflicting max value '{}' on @Group({}), previously specified {}", max, this.name, this.maxCallbackCount); } this.maxCallbackCount = Math.min(this.maxCallbackCount, max); } /** * Add a new member to this group * * @param member injector to add * @return fluent interface */ public InjectorGroupInfo add(InjectionInfo member) { this.members.add(member); return this; } /** * Validate all members in this group * * @return fluent interface * @throws InjectionValidationException if validation fails */ public InjectorGroupInfo validate() throws InjectionValidationException { if (this.members.size() == 0) { // I have no idea how we got here, but it's not an error :/ return this; } int total = 0; for (InjectionInfo member : this.members) { total += member.getInjectedCallbackCount(); } int min = this.getMinRequired(); int max = this.getMaxAllowed(); if (total < min) { throw new InjectionValidationException(this, String.format("expected %d invocation(s) but only %d succeeded", min, total)); } else if (total > max) { throw new InjectionValidationException(this, String.format("maximum of %d invocation(s) allowed but %d succeeded", max, total)); } return this; } }