    001    /*
    002     * Licensed to the Apache Software Foundation (ASF) under one or more
    003     * contributor license agreements. See the NOTICE file distributed with
    004     * this work for additional information regarding copyright ownership.
    005     * The ASF licenses this file to You under the Apache license, Version 2.0
    006     * (the "License"); you may not use this file except in compliance with
    007     * the License. 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     */
    017    package org.apache.logging.log4j.core.appender.db.jpa.converter;
    018
    019    import java.lang.reflect.Constructor;
    020    import java.lang.reflect.Field;
    021    import java.util.ArrayList;
    022    import java.util.Arrays;
    023    import java.util.List;
    024    import java.util.ListIterator;
    025
    026    import javax.persistence.AttributeConverter;
    027    import javax.persistence.Converter;
    028
    029    import org.apache.logging.log4j.core.util.Loader;
    030    import org.apache.logging.log4j.util.Strings;
    031
    032    /**
    033     * A JPA 2.1 attribute converter for {@link Throwable}s in {@link org.apache.logging.log4j.core.LogEvent}s. This
    034     * converter is capable of converting both to and from {@link String}s.
    035     */
    036    @Converter(autoApply = false)
    037    public class ThrowableAttributeConverter implements AttributeConverter<Throwable, String> {
    038        private static final int CAUSED_BY_STRING_LENGTH = 10;
    039
    040        private static final Field THROWABLE_CAUSE;
    041
    042        private static final Field THROWABLE_MESSAGE;
    043
    044        static {
    045            try {
    046                THROWABLE_CAUSE = Throwable.class.getDeclaredField("cause");
    047                THROWABLE_CAUSE.setAccessible(true);
    048                THROWABLE_MESSAGE = Throwable.class.getDeclaredField("detailMessage");
    049                THROWABLE_MESSAGE.setAccessible(true);
    050            } catch (final NoSuchFieldException e) {
    051                throw new IllegalStateException("Something is wrong with java.lang.Throwable.", e);
    052            }
    053        }
    054
    055        @Override
    056        public String convertToDatabaseColumn(final Throwable throwable) {
    057            if (throwable == null) {
    058                return null;
    059            }
    060
    061            final StringBuilder builder = new StringBuilder();
    062            this.convertThrowable(builder, throwable);
    063            return builder.toString();
    064        }
    065
    066        private void convertThrowable(final StringBuilder builder, final Throwable throwable) {
    067            builder.append(throwable.toString()).append('\n');
    068            for (final StackTraceElement element : throwable.getStackTrace()) {
    069                builder.append("\tat ").append(element).append('\n');
    070            }
    071            if (throwable.getCause() != null) {
    072                builder.append("Caused by ");
    073                this.convertThrowable(builder, throwable.getCause());
    074            }
    075        }
    076
    077        @Override
    078        public Throwable convertToEntityAttribute(final String s) {
    079            if (Strings.isEmpty(s)) {
    080                return null;
    081            }
    082
    083            final List<String> lines = Arrays.asList(s.split("(\n|\r\n)"));
    084            return this.convertString(lines.listIterator(), false);
    085        }
    086
    087        private Throwable convertString(final ListIterator<String> lines, final boolean removeCausedBy) {
    088            String firstLine = lines.next();
    089            if (removeCausedBy) {
    090                firstLine = firstLine.substring(CAUSED_BY_STRING_LENGTH);
    091            }
    092            final int colon = firstLine.indexOf(":");
    093            String throwableClassName;
    094            String message = null;
    095            if (colon > 1) {
    096                throwableClassName = firstLine.substring(0, colon);
    097                if (firstLine.length() > colon + 1) {
    098                    message = firstLine.substring(colon + 1).trim();
    099                }
    100            } else {
    101                throwableClassName = firstLine;
    102            }
    103
    104            final List<StackTraceElement> stackTrace = new ArrayList<StackTraceElement>();
    105            Throwable cause = null;
    106            while (lines.hasNext()) {
    107                final String line = lines.next();
    108
    109                if (line.startsWith("Caused by ")) {
    110                    lines.previous();
    111                    cause = convertString(lines, true);
    112                    break;
    113                }
    114
    115                stackTrace.add(
    116                        StackTraceElementAttributeConverter.convertString(line.trim().substring(3).trim())
    117                );
    118            }
    119
    120            return this.getThrowable(throwableClassName, message, cause,
    121                    stackTrace.toArray(new StackTraceElement[stackTrace.size()]));
    122        }
    123
    124        private Throwable getThrowable(final String throwableClassName, final String message, final Throwable cause,
    125                                       final StackTraceElement[] stackTrace) {
    126            try {
    127                @SuppressWarnings("unchecked")
    128                final Class<Throwable> throwableClass = (Class<Throwable>) Loader.loadClass(throwableClassName);
    129
    130                if (!Throwable.class.isAssignableFrom(throwableClass)) {
    131                    return null;
    132                }
    133
    134                Throwable throwable;
    135                if (message != null && cause != null) {
    136                    throwable = this.getThrowable(throwableClass, message, cause);
    137                    if (throwable == null) {
    138                        throwable = this.getThrowable(throwableClass, cause);
    139                        if (throwable == null) {
    140                            throwable = this.getThrowable(throwableClass, message);
    141                            if (throwable == null) {
    142                                throwable = this.getThrowable(throwableClass);
    143                                if (throwable != null) {
    144                                    THROWABLE_MESSAGE.set(throwable, message);
    145                                    THROWABLE_CAUSE.set(throwable, cause);
    146                                }
    147                            } else {
    148                                THROWABLE_CAUSE.set(throwable, cause);
    149                            }
    150                        } else {
    151                            THROWABLE_MESSAGE.set(throwable, message);
    152                        }
    153                    }
    154                } else if (cause != null) {
    155                    throwable = this.getThrowable(throwableClass, cause);
    156                    if (throwable == null) {
    157                        throwable = this.getThrowable(throwableClass);
    158                        if (throwable != null) {
    159                            THROWABLE_CAUSE.set(throwable, cause);
    160                        }
    161                    }
    162                } else if (message != null) {
    163                    throwable = this.getThrowable(throwableClass, message);
    164                    if (throwable == null) {
    165                        throwable = this.getThrowable(throwableClass);
    166                        if (throwable != null) {
    167                            THROWABLE_MESSAGE.set(throwable, cause);
    168                        }
    169                    }
    170                } else {
    171                    throwable = this.getThrowable(throwableClass);
    172                }
    173
    174                if (throwable == null) {
    175                    return null;
    176                }
    177                throwable.setStackTrace(stackTrace);
    178                return throwable;
    179            } catch (final Exception e) {
    180                return null;
    181            }
    182        }
    183
    184        private Throwable getThrowable(final Class<Throwable> throwableClass, final String message, final Throwable cause) {
    185            try {
    186                @SuppressWarnings("unchecked")
    187                final
    188                Constructor<Throwable>[] constructors = (Constructor<Throwable>[]) throwableClass.getConstructors();
    189                for (final Constructor<Throwable> constructor : constructors) {
    190                    final Class<?>[] parameterTypes = constructor.getParameterTypes();
    191                    if (parameterTypes.length == 2) {
    192                        if (String.class == parameterTypes[0] && Throwable.class.isAssignableFrom(parameterTypes[1])) {
    193                            return constructor.newInstance(message, cause);
    194                        } else if (String.class == parameterTypes[1] &&
    195                                Throwable.class.isAssignableFrom(parameterTypes[0])) {
    196                            return constructor.newInstance(cause, message);
    197                        }
    198                    }
    199                }
    200                return null;
    201            } catch (final Exception e) {
    202                return null;
    203            }
    204        }
    205
    206        private Throwable getThrowable(final Class<Throwable> throwableClass, final Throwable cause) {
    207            try {
    208                @SuppressWarnings("unchecked")
    209                final
    210                Constructor<Throwable>[] constructors = (Constructor<Throwable>[]) throwableClass.getConstructors();
    211                for (final Constructor<Throwable> constructor : constructors) {
    212                    final Class<?>[] parameterTypes = constructor.getParameterTypes();
    213                    if (parameterTypes.length == 1 && Throwable.class.isAssignableFrom(parameterTypes[0])) {
    214                        return constructor.newInstance(cause);
    215                    }
    216                }
    217                return null;
    218            } catch (final Exception e) {
    219                return null;
    220            }
    221        }
    222
    223        private Throwable getThrowable(final Class<Throwable> throwableClass, final String message) {
    224            try {
    225                return throwableClass.getConstructor(String.class).newInstance(message);
    226            } catch (final Exception e) {
    227                return null;
    228            }
    229        }
    230
    231        private Throwable getThrowable(final Class<Throwable> throwableClass) {
    232            try {
    233                return throwableClass.newInstance();
    234            } catch (final Exception e) {
    235                return null;
    236            }
    237        }
    238    }