All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.github.theangrydev.singletonenforcer.ConstructionCounter Maven / Gradle / Ivy

There is a newer version: 2.1.0
Show newest version
/*
 * Copyright 2016 Liam Williams .
 *
 * This file is part of singleton-enforcer.
 *
 * Licensed 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 io.github.theangrydev.singletonenforcer;

import net.bytebuddy.agent.ByteBuddyAgent;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.SuperMethodCall;
import net.bytebuddy.implementation.bind.annotation.AllArguments;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.This;
import net.bytebuddy.matcher.ElementMatcher.Junction;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;

import static java.lang.String.format;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
import static net.bytebuddy.matcher.ElementMatchers.*;

@SuppressWarnings("PMD.UseConcurrentHashMap") // intentionally using a single global lock
public class ConstructionCounter {

    private static final Object LOCK = new Object();

    private final String packageToCover;

    private final Map, List> classDependencies = new HashMap<>();
    private final Map>> dependencyUsage = new HashMap<>();

    private final Set seen =  new HashSet<>();
    private final Map, AtomicLong> constructionCounts = new HashMap<>();

    private ClassFileTransformer classFileTransformer;
    private Instrumentation instrumentation;

    public ConstructionCounter(String packageToCover) {
        this.packageToCover = packageToCover;
    }

    public void listenForConstructions() {
        Junction typeConditions = not(isInterface()).and(not(isSynthetic())).and(nameStartsWith(packageToCover));
        Junction constructorConditions = not(isBridge()).and(not(isSynthetic()));

        instrumentation = ByteBuddyAgent.getInstrumentation();
        classFileTransformer = new AgentBuilder.Default().type(typeConditions).transform((builder, typeDescription, classLoader) -> builder
                .constructor(constructorConditions)
                .intercept(SuperMethodCall.INSTANCE.andThen(MethodDelegation.to(this))))
                .installOn(instrumentation);
    }

    public void stopListeningForConstructions() {
        if (instrumentation == null) {
            throw new IllegalStateException("Need to start listening first!");
        }
        boolean removed = instrumentation.removeTransformer(classFileTransformer);
        if (!removed) {
            throw new IllegalStateException("Could not remove transformer");
        }
    }

    public Set> classesConstructedMoreThanOnce() {
        return constructionCounts.entrySet().stream()
                .filter(entry -> entry.getValue().longValue() > 1)
                .map(Map.Entry::getKey)
                .collect(toSet());
    }

    public List> dependencyUsageOutsideOf(Class singleton, Class typeOfDependencyThatShouldNotBeLeaked) {
        List dependencyThatShouldNotBeLeaked = classDependencies.get(singleton).stream()
                .filter(dependency -> typeOfDependencyThatShouldNotBeLeaked.isAssignableFrom(dependency.getClass()))
                .collect(toList());
        if (dependencyThatShouldNotBeLeaked.size() != 1) {
            throw new IllegalArgumentException(format("Type '%s' is not a singleton!", singleton));
        }
        return usagesThatAreNotBy(singleton, dependencyUsage.get(dependencyThatShouldNotBeLeaked.get(0)));
    }

    private List> usagesThatAreNotBy(Class target, List> usages) {
        return usages.stream().filter(aClass -> !aClass.equals(target)).collect(toList());
    }

    @SuppressWarnings("unused") // Invoked by ByteBuddy
    @RuntimeType
    public void intercept(@This Object object, @AllArguments Object... dependencies) {
        synchronized (LOCK) {
            recordDependencies(object.getClass(), dependencies);
            recordUsage(object.getClass(), dependencies);
            boolean alreadySeen = !seen.add(object);
            if (alreadySeen) {
                return;
            }
            AtomicLong atomicLong = constructionCounts.putIfAbsent(object.getClass(), new AtomicLong(1));
            if (atomicLong != null) {
                atomicLong.incrementAndGet();
            }
        }
    }

    @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") // can't help it
    private void recordUsage(Class aClass, Object... dependencies) {
        for (Object dependency : dependencies) {
            List> classes = dependencyUsage.get(dependency);
            if (classes == null) {
                classes = new ArrayList<>();
                dependencyUsage.put(dependency, classes);
            }
            classes.add(aClass);
        }
    }

    private void recordDependencies(Class aClass, Object... dependencies) {
        List objects = classDependencies.get(aClass);
        if (objects == null) {
            objects = new ArrayList<>();
            classDependencies.put(aClass, objects);
        }
        Collections.addAll(objects, dependencies);
    }
}