001/*
002 * Configurate
003 * Copyright (C) zml and Configurate contributors
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *    http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.spongepowered.configurate.objectmapping.meta;
018
019import io.leangen.geantyref.GenericTypeReflector;
020import org.checkerframework.checker.nullness.qual.Nullable;
021import org.spongepowered.configurate.serialize.SerializationException;
022import org.spongepowered.configurate.util.Types;
023
024import java.lang.annotation.Annotation;
025import java.lang.reflect.InvocationTargetException;
026import java.lang.reflect.Method;
027import java.lang.reflect.Modifier;
028import java.lang.reflect.Type;
029import java.util.ArrayList;
030import java.util.List;
031
032/**
033 * A callback executed after all field serialization has been performed.
034 *
035 * @since 4.2.0
036 */
037@FunctionalInterface
038public interface PostProcessor {
039
040    /**
041     * Perform post-processing on the fully deserialized instance.
042     *
043     * @param instance the instance to post-process
044     * @throws SerializationException if the underlying operation
045     *     detects an error
046     * @since 4.2.0
047     */
048    void postProcess(Object instance) throws SerializationException;
049
050    /**
051     * A factory to produce object post-processors.
052     *
053     * @since 4.2.0
054     */
055    @FunctionalInterface
056    interface Factory {
057
058        /**
059         * Return a post-processor if any is applicable to the provided type.
060         *
061         * @param type the type to post-process
062         * @return a potential post-processor
063         * @throws SerializationException if there is a declared post-processor
064         *     handled by this factory with an invalid type
065         * @since 4.2.0
066         */
067        @Nullable PostProcessor createProcessor(Type type) throws SerializationException;
068
069    }
070
071    /**
072     * Discover methods annotated with the designated post-processor annotation
073     * in object-mapped types and their supertypes.
074     *
075     * <p>Annotated methods must be non-static, take no parameters, and can have
076     * no declared thrown exceptions
077     * except for {@link SerializationException}.</p>
078     *
079     * @param annotation the annotation that will mark post-processor methods
080     * @return a factory for annotated methods
081     * @since 4.2.0
082     */
083    static Factory methodsAnnotated(final Class<? extends Annotation> annotation) {
084        return type -> {
085            List<Method> methods = null;
086            for (final Method method : Types.allDeclaredMethods(GenericTypeReflector.erase(type))) {
087                if (method.isAnnotationPresent(annotation)) {
088                    // Validate method
089                    final int modifiers = method.getModifiers();
090                    if (Modifier.isAbstract(modifiers)) {
091                        continue;
092                    }
093
094                    if (Modifier.isStatic(modifiers)) {
095                        throw new SerializationException(
096                            type,
097                            "Post-processor method " + method.getName() + "() annotated @" + annotation.getSimpleName()
098                                + " must not be static."
099                        );
100                    }
101                    if (method.getParameterCount() != 0) {
102                        throw new SerializationException(
103                            type,
104                            "Post-processor method " + method.getName() + "() annotated @" + annotation.getSimpleName()
105                                + " must not take any parameters."
106                        );
107                    }
108
109                    for (final Class<?> exception : method.getExceptionTypes()) {
110                        if (!SerializationException.class.isAssignableFrom(exception)) {
111                            throw new SerializationException(
112                                type,
113                                "Post-processor method " + method.getName() + "() annotated @" + annotation.getSimpleName()
114                                    + " must only throw SerializationException or its subtypes, but is declared to throw "
115                                    + exception.getSimpleName() + "."
116                            );
117                        }
118                    }
119                    method.setAccessible(true);
120
121                    // Then add it
122                    if (methods == null) {
123                        methods = new ArrayList<>();
124                    }
125                    methods.add(method);
126                }
127            }
128
129            if (methods != null) {
130                final List<Method> finalMethods = methods;
131                return instance -> {
132                    SerializationException aggregateException = null;
133                    for (final Method postProcessorMethod : finalMethods) {
134                        SerializationException exc = null;
135                        try {
136                            postProcessorMethod.invoke(instance);
137                        } catch (final InvocationTargetException ex) {
138                            if (ex.getCause() instanceof SerializationException) {
139                                exc = (SerializationException) ex.getCause();
140                                exc.initType(type);
141                            } else if (ex.getCause() != null) {
142                                exc = new SerializationException(
143                                    type,
144                                    "Failure occurred in post-processor method " + postProcessorMethod.getName() + "()", ex.getCause()
145                                );
146                            } else {
147                                exc = new SerializationException(
148                                    type,
149                                    "Unknown error occurred attempting to invoke post-processor method " + postProcessorMethod.getName() + "()",
150                                    ex
151                                );
152                            }
153                        } catch (final IllegalAccessException | IllegalArgumentException ex) {
154                            exc = new SerializationException(
155                                type, "Failed to invoke post-processor method " + postProcessorMethod.getName() + "()", ex
156                            );
157                        }
158
159                        // Capture all relevant exceptions
160                        if (exc != null) {
161                            if (aggregateException == null) {
162                                aggregateException = exc;
163                            } else {
164                                aggregateException.addSuppressed(exc);
165                            }
166                        }
167                    }
168
169                    // If anybody threw an exception, rethrow
170                    if (aggregateException != null) {
171                        throw aggregateException;
172                    }
173                };
174            }
175
176            return null;
177        };
178    }
179
180    /**
181     * Discover methods annotated with the {@link PostProcess} annotation.
182     *
183     * <p>All restrictions from {@link #methodsAnnotated(Class)} apply to these
184     * annotated methods.</p>
185     *
186     * @return a new factory for discovering annotated methods
187     * @see #methodsAnnotated(Class)
188     * @since 4.2.0
189     */
190    static Factory methodsAnnotatedPostProcess() {
191        return methodsAnnotated(PostProcess.class);
192    }
193
194}