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 */
017package org.apache.logging.log4j.core.appender.db.jpa.converter;
018
019import java.lang.reflect.Constructor;
020import java.lang.reflect.Field;
021import java.util.ArrayList;
022import java.util.Arrays;
023import java.util.List;
024import java.util.ListIterator;
025
026import javax.persistence.AttributeConverter;
027import javax.persistence.Converter;
028
029import org.apache.logging.log4j.core.helpers.Strings;
030
031/**
032 * A JPA 2.1 attribute converter for {@link Throwable}s in {@link org.apache.logging.log4j.core.LogEvent}s. This
033 * converter is capable of converting both to and from {@link String}s.
034 */
035@Converter(autoApply = false)
036public class ThrowableAttributeConverter implements AttributeConverter<Throwable, String> {
037    private static final int CAUSED_BY_STRING_LENGTH = 10;
038
039    private static final Field THROWABLE_CAUSE;
040
041    private static final Field THROWABLE_MESSAGE;
042
043    static {
044        try {
045            THROWABLE_CAUSE = Throwable.class.getDeclaredField("cause");
046            THROWABLE_CAUSE.setAccessible(true);
047            THROWABLE_MESSAGE = Throwable.class.getDeclaredField("detailMessage");
048            THROWABLE_MESSAGE.setAccessible(true);
049        } catch (final NoSuchFieldException e) {
050            throw new IllegalStateException("Something is wrong with java.lang.Throwable.", e);
051        }
052    }
053
054    @Override
055    public String convertToDatabaseColumn(final Throwable throwable) {
056        if (throwable == null) {
057            return null;
058        }
059
060        final StringBuilder builder = new StringBuilder();
061        this.convertThrowable(builder, throwable);
062        return builder.toString();
063    }
064
065    private void convertThrowable(final StringBuilder builder, final Throwable throwable) {
066        builder.append(throwable.toString()).append('\n');
067        for (final StackTraceElement element : throwable.getStackTrace()) {
068            builder.append("\tat ").append(element).append('\n');
069        }
070        if (throwable.getCause() != null) {
071            builder.append("Caused by ");
072            this.convertThrowable(builder, throwable.getCause());
073        }
074    }
075
076    @Override
077    public Throwable convertToEntityAttribute(final String s) {
078        if (Strings.isEmpty(s)) {
079            return null;
080        }
081
082        final List<String> lines = Arrays.asList(s.split("(\n|\r\n)"));
083        return this.convertString(lines.listIterator(), false);
084    }
085
086    private Throwable convertString(final ListIterator<String> lines, final boolean removeCausedBy) {
087        String firstLine = lines.next();
088        if (removeCausedBy) {
089            firstLine = firstLine.substring(CAUSED_BY_STRING_LENGTH);
090        }
091        final int colon = firstLine.indexOf(":");
092        String throwableClassName;
093        String message = null;
094        if (colon > 1) {
095            throwableClassName = firstLine.substring(0, colon);
096            if (firstLine.length() > colon + 1) {
097                message = firstLine.substring(colon + 1).trim();
098            }
099        } else {
100            throwableClassName = firstLine;
101        }
102
103        final List<StackTraceElement> stackTrace = new ArrayList<StackTraceElement>();
104        Throwable cause = null;
105        while (lines.hasNext()) {
106            final String line = lines.next();
107
108            if (line.startsWith("Caused by ")) {
109                lines.previous();
110                cause = convertString(lines, true);
111                break;
112            }
113
114            stackTrace.add(
115                    StackTraceElementAttributeConverter.convertString(line.trim().substring(3).trim())
116            );
117        }
118
119        return this.getThrowable(throwableClassName, message, cause,
120                stackTrace.toArray(new StackTraceElement[stackTrace.size()]));
121    }
122
123    private Throwable getThrowable(final String throwableClassName, final String message, final Throwable cause,
124                                   final StackTraceElement[] stackTrace) {
125        try {
126            @SuppressWarnings("unchecked")
127            final
128            Class<Throwable> throwableClass = (Class<Throwable>) Class.forName(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}