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.util.Loader;
030import 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)
037public 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}