public abstract class AbstractObservableCacheServiceProcessor extends AbstractProcessor {

    private List<BaseMethod> baseMethods;
    private List<ExtendMethod> extendMethods;

    protected abstract List<BaseMethod> baseMethods();

    protected abstract List<ExtendMethod> extendMethods();

    protected abstract Class<? extends Annotation> serviceAnnotation();

    protected abstract String observableCachePackage();

    protected abstract String servicePackage();

    protected abstract String observableCacheClassName();

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        baseMethods = baseMethods();
        extendMethods = extendMethods();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        try {
            processThrowing(annotations, roundEnv);
        } catch (Throwable e) {
            processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, e.getMessage());
        }
        return true;
    }

    private void processThrowing(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        List<TypeElement> services = roundEnv.getElementsAnnotatedWith(serviceAnnotation()).stream()
                .filter(element -> element.getKind().equals(ElementKind.INTERFACE))
                .map(element -> (TypeElement) element)
                .peek(observableCacheServiceInterfaceTypeElement -> {
                    TypeSpec.Builder observableCacheServiceImplBuilder = TypeSpec.classBuilder(observableCacheServiceInterfaceTypeElement.getSimpleName() + "Impl");
                    implementMethods(observableCacheServiceImplBuilder, observableCacheServiceInterfaceTypeElement);
                    writeService(observableCacheServiceImplBuilder, observableCacheServiceInterfaceTypeElement);
                })
                .collect(Collectors.toList());
        if (!services.isEmpty()) {
            writeCreator(services);
        }
    }

    private void implementMethods(TypeSpec.Builder builder, TypeElement observableCacheServiceInterfaceTypeElement) {
        List<ExecutableElement> methods = observableCacheServiceInterfaceTypeElement.getEnclosedElements().stream()
                .filter(element -> element.getKind().equals(ElementKind.METHOD))
                .map(element -> (ExecutableElement) element)
                .collect(Collectors.toList());
        Deque<ExecutableElement> methodsQueue = new ArrayDeque<>(methods);

        for (int i = 1; i <= methods.size(); i++) {
            ExecutableElement baseElement = methodsQueue.pop();

            Optional<BaseMethod> baseMethodOptional = baseMethods.stream()
                    .filter(baseMethod -> baseMethod.isBaseMethod(baseElement, processingEnv))
                    .findFirst();

            if (baseMethodOptional.isPresent()) {
                BaseMethod baseMethod = baseMethodOptional.get();

                String keyValue = baseMethod.keyValue(observableCacheServiceInterfaceTypeElement, baseElement);
                String keyName = baseMethod.keyName(baseElement);
                baseMethod.writeMethod(builder, baseElement, keyName);
                builder.addField(FieldSpec.builder(String.class, keyName)
                        .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
                        .initializer("$S", keyValue)
                        .build());

                Iterator<ExecutableElement> methodQueueIterator = methodsQueue.iterator();
                while (methodQueueIterator.hasNext()) {
                    ExecutableElement methodToImplement = methodQueueIterator.next();
                    extendMethods.stream()
                            .filter(extendMethod -> extendMethod.isExtendMethod(baseElement, baseMethod, methodToImplement, processingEnv))
                            .findFirst()
                            .ifPresent(extendMethod -> {
                                methodQueueIterator.remove();
                                extendMethod.writeMethod(builder, methodToImplement, keyName);
                            });
                }
            } else {
                methodsQueue.add(baseElement);
            }
            if (methodsQueue.isEmpty()) {
                break;
            }
        }
        if (!methodsQueue.isEmpty()) {
            throw new ObservableCacheServiceException("Unable to implement methods for " + observableCacheServiceInterfaceTypeElement.getQualifiedName().toString());
        }
    }

    private void writeService(TypeSpec.Builder builder, TypeElement observableCacheServiceInterfaceTypeElement) {
        builder.addSuperinterface(TypeName.get(observableCacheServiceInterfaceTypeElement.asType()))
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addField(FieldSpec.builder(ClassName.get(observableCachePackage(), observableCacheClassName()), "observableCache")
                        .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
                        .build())
                .addMethod(MethodSpec.constructorBuilder()
                        .addModifiers(Modifier.PUBLIC)
                        .addParameter(ClassName.get(observableCachePackage(), observableCacheClassName()), "observableCache")
                        .addStatement("this.$N = $N", "observableCache", "observableCache")
                        .build());

        PackageElement packageElement = processingEnv.getElementUtils().getPackageOf(observableCacheServiceInterfaceTypeElement);
        JavaFile observableCacheServiceImplFile = JavaFile.builder(packageElement.getQualifiedName().toString(), builder.build()).build();
        try {
            observableCacheServiceImplFile.writeTo(processingEnv.getFiler());
        } catch (IOException e) {
            throw new ObservableCacheServiceException("Cannot write class" + observableCacheServiceInterfaceTypeElement.getSimpleName(), e);
        }
    }

    private void writeCreator(List<TypeElement> services) {
        TypeElement observableCacheServiceCreatorTypeElement = processingEnv.getElementUtils().getTypeElement(servicePackage() + ".ObservableCacheServiceCreator");
        observableCacheServiceCreatorTypeElement.getInterfaces().stream()
                .map(superInterface -> processingEnv.getTypeUtils().asElement(superInterface))
                .filter(element -> element.getKind().equals(ElementKind.INTERFACE))
                .map(element -> (TypeElement) element)
                .map(TypeElement::getEnclosedElements)
                .flatMap(Collection::stream)
                .filter(element -> element.getKind().equals(ElementKind.METHOD))
                .map(element -> (ExecutableElement) element)
                .findAny()
                .ifPresent(methodToOverride -> {
                    MethodSpec.Builder methodBuilder = MethodSpec.overriding(methodToOverride, processingEnv.getTypeUtils().getDeclaredType(observableCacheServiceCreatorTypeElement), processingEnv.getTypeUtils());
                    services.forEach(service -> {
                        PackageElement packageElement = processingEnv.getElementUtils().getPackageOf(service);
                        methodBuilder.beginControlFlow("if(arg0.equals($T.class))", TypeName.get(service.asType()))
                                .addStatement("return (S) new $T(arg1)", ClassName.get(packageElement.getQualifiedName().toString(), service.getSimpleName() + "Impl"))
                                .endControlFlow();
                    });
                    JavaFile observableCacheServiceCreatorImplFile = JavaFile.builder(servicePackage(),
                            TypeSpec.classBuilder("ObservableCacheServiceCreatorImpl")
                                    .addSuperinterface(ClassName.get(servicePackage(), "ObservableCacheServiceCreator"))
                                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                                    .addMethod(methodBuilder
                                            .addStatement("throw new $T($S + arg0)", IllegalArgumentException.class, "Cannot create service for ")
                                            .build())
                                    .build())
                            .build();
                    try {
                        observableCacheServiceCreatorImplFile.writeTo(processingEnv.getFiler());
                    } catch (IOException e) {
                        throw new ObservableCacheServiceException("Cannot write class ObservableCacheServiceCreator", e);
                    }
                });
    }
} public interface BaseMethod extends WritableMethod {

    default boolean isBaseMethod(ExecutableElement method, ProcessingEnvironment processingEnvironment) {
        if (!method.getParameters().isEmpty()) {
            return false;
        }
        TypeElement returnedElement = (TypeElement) processingEnvironment.getTypeUtils().asElement(method.getReturnType());
        if (!returnedElement.getQualifiedName().toString().contains(returnedType())) {
            return false;
        }
        return true;
    }

    default String keyName(ExecutableElement method) {
        String camelCaseName = method.getSimpleName().toString();
        return CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, camelCaseName);
    }

    default String keyValue(TypeElement type, ExecutableElement method) {
        return type.getQualifiedName().toString() + "." + method.getSimpleName().toString();
    }

    @Override
    default void writeMethod(TypeSpec.Builder builder, ExecutableElement method, String keyName) {
        builder.addMethod(MethodSpec.overriding(method)
                .addStatement("return observableCache." + cacheMethod() + "(" + keyName + ")")
                .build());
    }

    String returnedType();

    String cacheMethod();
} public interface ExtendMethod extends WritableMethod {

    default boolean isExtendMethod(ExecutableElement baseMethodElement, BaseMethod baseMethod, ExecutableElement extendMethod, ProcessingEnvironment processingEnvironment) {
        List<String> baseMethodTokens = nameTokens(baseMethodElement);
        List<String> extendMethodTokens = nameTokens(extendMethod);
        if (baseMethodTokens.size() >= extendMethodTokens.size()) {
            return false;
        }
        boolean baseHasTokens = false;
        for (int i = 0; i < extendMethodTokens.size(); i++) {
            String token = extendMethodTokens.remove(i);
            String lowerCaseToken = token.toLowerCase();
            String newFirstItem = extendMethodTokens.get(0);
            String newFirstItemLowerCase = newFirstItem.toLowerCase();
            extendMethodTokens.set(0, newFirstItemLowerCase);
            if (lowerCaseToken.equals(additionalToken()) && extendMethodTokens.equals(baseMethodTokens)) {
                baseHasTokens = true;
            }
            extendMethodTokens.set(0, newFirstItem);
            extendMethodTokens.add(i, token);
            if (baseHasTokens) {
                break;
            }
        }
        return baseHasTokens;
    }

    String additionalToken();

} public class RemoveExtendMethod implements ExtendMethod {

    @Override
    public boolean isExtendMethod(ExecutableElement baseMethodElement, BaseMethod baseMethod, ExecutableElement extendMethod, ProcessingEnvironment processingEnvironment) {
        if (!extendMethod.getParameters().isEmpty()) {
            return false;
        }
        if (!extendMethod.getReturnType().getKind().equals(TypeKind.BOOLEAN)) {
            return false;
        }
        return ExtendMethod.super.isExtendMethod(baseMethodElement, baseMethod, extendMethod, processingEnvironment);
    }

    @Override
    public String additionalToken() {
        return "remove";
    }

    @Override
    public void writeMethod(TypeSpec.Builder builder, ExecutableElement method, String keyName) {
        builder.addMethod(MethodSpec.overriding(method)
                .addStatement("return observableCache.remove(" + keyName + ")")
                .build());
    }

} public class CompletableBaseMethod implements BaseMethod {

    @Override
    public String returnedType() {
        return "com.github.aleksandermielczarek.observablecache.CacheableCompletable";
    }

    @Override
    public String cacheMethod() {
        return "cacheCompletable";
    }

} public class ObservableBaseMethod implements BaseMethod {

    @Override
    public String returnedType() {
        return "com.github.aleksandermielczarek.observablecache.CacheableObservable";
    }

    @Override
    public String cacheMethod() {
        return "cacheObservable";
    }

} public class SingleBaseMethod implements BaseMethod {

    @Override
    public String returnedType() {
        return "com.github.aleksandermielczarek.observablecache.CacheableSingle";
    }

    @Override
    public String cacheMethod() {
        return "cacheSingle";
    }

} public class CompletableExtendMethod implements FromCacheMethod {

    @Override
    public String additionalToken() {
        return "cached";
    }

    @Override
    public String returnedBaseType() {
        return "com.github.aleksandermielczarek.observablecache.CacheableCompletable";
    }

    @Override
    public String returnedType() {
        return "com.github.aleksandermielczarek.observablecache.CompletableFromCache";
    }

    @Override
    public String fromCacheMethod() {
        return "getCompletable";
    }

} public class ObservableExtendMethod implements FromCacheMethod {

    @Override
    public String additionalToken() {
        return "cached";
    }

    @Override
    public String returnedBaseType() {
        return "com.github.aleksandermielczarek.observablecache.CacheableObservable";
    }

    @Override
    public String returnedType() {
        return "com.github.aleksandermielczarek.observablecache.ObservableFromCache";
    }

    @Override
    public String fromCacheMethod() {
        return "getObservable";
    }

} public class SingleExtendMethod implements FromCacheMethod {

    @Override
    public String additionalToken() {
        return "cached";
    }

    @Override
    public String returnedBaseType() {
        return "com.github.aleksandermielczarek.observablecache.CacheableSingle";
    }

    @Override
    public String returnedType() {
        return "com.github.aleksandermielczarek.observablecache.SingleFromCache";
    }

    @Override
    public String fromCacheMethod() {
        return "getSingle";
    }

}
import com.github.aleksandermielczarek.observablecache.service.annotations.ObservableCacheService;
import com.github.aleksandermielczarek.observablecache.service.processor.api.AbstractObservableCacheServiceProcessor;
import com.github.aleksandermielczarek.observablecache.service.processor.api.method.base.BaseMethod;
import com.github.aleksandermielczarek.observablecache.service.processor.api.method.extend.ExtendMethod;
import com.github.aleksandermielczarek.observablecache.service.processor.api.method.extend.RemoveExtendMethod;
import com.github.aleksandermielczarek.observablecache.service.processor.method.base.CompletableBaseMethod;
import com.github.aleksandermielczarek.observablecache.service.processor.method.base.ObservableBaseMethod;
import com.github.aleksandermielczarek.observablecache.service.processor.method.base.SingleBaseMethod;
import com.github.aleksandermielczarek.observablecache.service.processor.method.extend.CompletableExtendMethod;
import com.github.aleksandermielczarek.observablecache.service.processor.method.extend.ObservableExtendMethod;
import com.github.aleksandermielczarek.observablecache.service.processor.method.extend.SingleExtendMethod;
import com.google.auto.service.AutoService;
import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.List;
import javax.annotation.processing.Processor;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
package com.github.aleksandermielczarek.observablecache.service.processor;




/**
 * Created by Aleksander Mielczarek on 30.10.2016.
 */
@AutoService(Processor.class)
@SupportedAnnotationTypes("com.github.aleksandermielczarek.observablecache.service.annotations.ObservableCacheService")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class ObservableCacheServiceProcessor extends AbstractObservableCacheServiceProcessor {

    @Override
    protected List<BaseMethod> baseMethods() {
        return Arrays.asList(new CompletableBaseMethod(), new ObservableBaseMethod(), new SingleBaseMethod());
    }

    @Override
    protected List<ExtendMethod> extendMethods() {
        return Arrays.asList(new CompletableExtendMethod(), new ObservableExtendMethod(), new RemoveExtendMethod(), new SingleExtendMethod());