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 */
037public interface PostProcessor {
038
039    /**
040     * Perform post-processing on the fully deserialized instance.
041     *
042     * @param instance the instance to post-process
043     * @throws SerializationException if the underlying operation
044     *     detects an error
045     * @since 4.2.0
046     */
047    void postProcess(Object instance) throws SerializationException;
048
049    /**
050     * A factory to produce object post-processors.
051     *
052     * @since 4.2.0
053     */
054    interface Factory {
055
056        /**
057         * Return a post-processor if any is applicable to the provided type.
058         *
059         * @param type the type to post-process
060         * @return a potential post-processor
061         * @throws SerializationException if there is a declared post-processor
062         *     handled by this factory with an invalid type
063         * @since 4.2.0
064         */
065        @Nullable PostProcessor createProcessor(Type type) throws SerializationException;
066
067    }
068
069    /**
070     * Discover methods annotated with the designated post-processor annotation
071     * in object-mapped types and their supertypes.
072     *
073     * <p>Annotated methods must be non-static, take no parameters, and can have
074     * no declared thrown exceptions
075     * except for {@link SerializationException}.</p>
076     *
077     * @param annotation the annotation that will mark post-processor methods
078     * @return a factory for annotated methods
079     * @since 4.2.0
080     */
081    static Factory methodsAnnotated(final Class<? extends Annotation> annotation) {
082        return type -> {
083            List<Method> methods = null;
084            for (final Method method : Types.allDeclaredMethods(GenericTypeReflector.erase(type))) {
085                if (method.isAnnotationPresent(annotation)) {
086                    // Validate method
087                    final int modifiers = method.getModifiers();
088                    if (Modifier.isAbstract(modifiers)) {
089                        continue;
090                    }
091
092                    if (Modifier.isStatic(modifiers)) {
093                        throw new SerializationException(
094                            type,
095                            "Post-processor method " + method.getName() + "() annotated @" + annotation.getSimpleName()
096                                + " must not be static."
097                        );
098                    }
099                    if (method.getParameterCount() != 0) {
100                        throw new SerializationException(
101                            type,
102                            "Post-processor method " + method.getName() + "() annotated @" + annotation.getSimpleName()
103                                + " must not take any parameters."
104                        );
105                    }
106
107                    for (final Class<?> exception : method.getExceptionTypes()) {
108                        if (!SerializationException.class.isAssignableFrom(exception)) {
109                            throw new SerializationException(
110                                type,
111                                "Post-processor method " + method.getName() + "() annotated @" + annotation.getSimpleName()
112                                    + " must only throw SerializationException or its subtypes, but is declared to throw "
113                                    + exception.getSimpleName() + "."
114                            );
115                        }
116                    }
117                    method.setAccessible(true);
118
119                    // Then add it
120                    if (methods == null) {
121                        methods = new ArrayList<>();
122                    }
123                    methods.add(method);
124                }
125            }
126
127            if (methods != null) {
128                final List<Method> finalMethods = methods;
129                return instance -> {
130                    SerializationException aggregateException = null;
131                    for (final Method postProcessorMethod : finalMethods) {
132                        SerializationException exc = null;
133                        try {
134                            postProcessorMethod.invoke(instance);
135                        } catch (final InvocationTargetException ex) {
136                            if (ex.getCause() instanceof SerializationException) {
137                                exc = (SerializationException) ex.getCause();
138                                exc.initType(type);
139                            } else if (ex.getCause() != null) {
140                                exc = new SerializationException(
141                                    type,
142                                    "Failure occurred in post-processor method " + postProcessorMethod.getName() + "()", ex.getCause()
143                                );
144                            } else {
145                                exc = new SerializationException(
146                                    type,
147                                    "Unknown error occurred attempting to invoke post-processor method " + postProcessorMethod.getName() + "()",
148                                    ex
149                                );
150                            }
151                        } catch (final IllegalAccessException | IllegalArgumentException ex) {
152                            exc = new SerializationException(
153                                type, "Failed to invoke post-processor method " + postProcessorMethod.getName() + "()", ex
154                            );
155                        }
156
157                        // Capture all relevant exceptions
158                        if (exc != null) {
159                            if (aggregateException == null) {
160                                aggregateException = exc;
161                            } else {
162                                aggregateException.addSuppressed(exc);
163                            }
164                        }
165                    }
166
167                    // If anybody threw an exception, rethrow
168                    if (aggregateException != null) {
169                        throw aggregateException;
170                    }
171                };
172            }
173
174            return null;
175        };
176    }
177
178    /**
179     * Discover methods annotated with the {@link PostProcess} annotation.
180     *
181     * <p>All restrictions from {@link #methodsAnnotated(Class)} apply to these
182     * annotated methods.</p>
183     *
184     * @return a new factory for discovering annotated methods
185     * @see #methodsAnnotated(Class)
186     * @since 4.2.0
187     */
188    static Factory methodsAnnotatedPostProcess() {
189        return methodsAnnotated(PostProcess.class);
190    }
191
192}