/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.openjpa.util;

import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Serializable;
import java.util.Collection;
import java.util.Iterator;

import org.apache.openjpa.conf.OpenJPAVersion;
import org.apache.openjpa.enhance.PersistenceCapable;
import org.apache.openjpa.lib.util.J2DoPrivHelper;
import org.apache.openjpa.lib.util.JavaVersions;

/**
 * Utility methods for externalizing and handling exceptions.
 *
 * @author Marc Prud'hommeaux
 * @since 0.2.5
 */
public class Exceptions {

    public static final Throwable[] EMPTY_THROWABLES = new Throwable[0];

    static final String SEP = J2DoPrivHelper.getLineSeparator();

    private static final OutputStream DEV_NULL = new OutputStream() {
        @Override
        public void write(int b) {
        }
    };

    /**
     * Test to see if the specified object will be able to be serialized. This
     * will check if the object implements {@link Serializable}, and if so,
     * will try to perform an actual serialization. This is in case the object
     * has fields which, in turn, are not serializable.
     *
     * @param ob the object to test
     * @return true if the object will be able to be serialized
     */
    private static boolean isSerializable(Object ob) {
        if (!(ob instanceof Serializable))
            return false;

        // don't serialize persistent objects exceptions to prevent
        // reading in all the state
        if (!ImplHelper.isManagedType(null, ob.getClass()))
            return false;

        // now do an actual test to see if we will be
        // able to perform the serialization
        try {
            new ObjectOutputStream(DEV_NULL).writeObject(ob);
            return true;
        } catch (Throwable t) {
            return false;
        }
    }

    /**
     * Safely stringify the given object.
     */
    public static String toString(Object ob) {
        if (ob == null)
            return "null";

        // don't take oid of new objects since it can cause a flush if auto-inc
        // and the id is meaningless anyway
        Object oid = getObjectId(ob);

        if (oid != null) {
            if (oid instanceof Id)
                return oid.toString();
            String oidString = oid.toString();
            // some oids stringify their class names. Some do not.
            if (oidString.indexOf(ob.getClass().getName()) == -1) {
                return ob.getClass().getName() + "-" + oidString;
            } else {
                return oidString;
            }
        }

        if (ImplHelper.isManagedType(null, ob.getClass())) {
            // never call toString() on a PersistenceCapable, since
            // it may access persistent fields; fall-back to using
            // the standard object stringification mechanism. New
            // instances that use proxying (property-access instances,
            // for example) that were created with the 'new' keyword
            // will not end up in this code, which is ok since they
            // don't do lazy loading anyways, so they will stringify
            // safely.
            return ob.getClass().getName() + "@"
                + Integer.toHexString(System.identityHashCode(ob));
        }

        try {
            String s = ob.toString();
            if (s.indexOf(ob.getClass().getName()) == -1)
                s += " [" + ob.getClass().getName() + "]";
            return s;
        } catch (Throwable t) {
            return ob.getClass().getName();
        }
    }

    /**
     * Safely stringify the given objects.
     */
    public static String toString(Collection failed) {
        StringBuilder buf = new StringBuilder();
        buf.append("[");
        for (Iterator itr = failed.iterator(); itr.hasNext();) {
            buf.append(Exceptions.toString(itr.next()));
            if (itr.hasNext())
                buf.append(", ");
        }
        buf.append("]");
        return buf.toString();
    }

    /**
     * Stringify the given exception.
     */
    public static String toString(ExceptionInfo e) {
        int type = e.getType();
        StringBuilder buf = new StringBuilder();
        buf.append("<").
            append(OpenJPAVersion.VERSION_ID).
            append(' ').
            append(e.isFatal() ? "fatal " : "nonfatal ").
            append (type == ExceptionInfo.GENERAL ? "general error" :
                type == ExceptionInfo.INTERNAL ? "internal error" :
                type == ExceptionInfo.STORE ? "store error" :
                type == ExceptionInfo.UNSUPPORTED ? "unsupported error" :
                type == ExceptionInfo.USER ? "user error" :
                (type + " error")).
            append("> ");
        buf.append(e.getClass().getName()).append(": ").
            append(e.getMessage());
        Object failed = e.getFailedObject();
        if (failed != null)
            buf.append(SEP).append("FailedObject: ").
                append(toString(failed));
        return buf.toString();
    }

    /**
     * Print the stack trace of the exception's nested throwables.
     */
    public static void printNestedThrowables(ExceptionInfo e, PrintStream out) {
        // if this is Java 1.4 and there is exactly a single
        // exception, then defer to 1.4's behavior of printing
        // out the result of getCause(). This deferral happens in
        // the calling code.
        Throwable[] nested = e.getNestedThrowables();
        int i = (JavaVersions.VERSION >= 4) ? 1 : 0;
        if (i < nested.length) {
            out.println("NestedThrowables:");
            for (; i < nested.length; i++)
                // guard against a nasty null in the array
                if (nested[i] != null)
                    nested[i].printStackTrace(out);
        }
    }

    /**
     * Print the stack trace of the exception's nested throwables.
     */
    public static void printNestedThrowables(ExceptionInfo e, PrintWriter out) {
        // if this is Java 1.4 and there is exactly a single
        // exception, then defer to 1.4's behavior of printing
        // out the result of getCause(). This deferral happens in
        // the calling code.
        Throwable[] nested = e.getNestedThrowables();
        int i = (JavaVersions.VERSION >= 4) ? 1 : 0;
        if (i < nested.length) {
            out.println("NestedThrowables:");
            for (; i < nested.length; i++)
                // guard against a nasty null in the array
                if (nested[i] != null)
                    nested[i].printStackTrace(out);
        }
    }

    /**
     * Convert the specified failed object into a serializable
     * object for when we are serializing an Exception. It will
     * try the following:
     * <ul>
     * <li>if the object can be serialized, return the object itself</li>
     * <li>if the object has a serializable oid, return the oid</li>
     * <li>if the object has a non-serializable oid, return the oid's
     * toString and the object class</li>
     * <li>return the object's toString</li>
     * </ul>
     *
     * @param ob the object to convert
     * @return some serialized representation of the object
     */
    public static Object replaceFailedObject(Object ob) {
        if (ob == null)
            return null;
        if (isSerializable(ob))
            return ob;

        // don't take oid of new objects since it can cause a flush if auto-inc
        // and the id is meaningless anyway
        Object oid = getObjectId(ob);
        if (oid != null && isSerializable(oid))
            return oid;

        // last ditch: stringify the object
        return toString(ob);
    }

    /**
     * Convert the specified throwables into a serialzable array. If
     * any of the nested throwables cannot be serialized, they will
     * be converted into a Exception with the original message.
     */
    public static Throwable[] replaceNestedThrowables(Throwable[] nested) {
        if (nested == null || nested.length == 0)
            return nested;
        if (isSerializable(nested))
            return nested;

        Throwable[] newNested = new Throwable[nested.length];
        for (int i = 0; i < nested.length; i++) {
            if (isSerializable(nested[i]))
                newNested[i] = nested[i];
            else
                // guard against a nasty null in the array by using valueOf
                // instead of toString to prevent throwing yet another
                // exception
                newNested[i] = new Exception(String.valueOf(nested[i]));
        }
        return newNested;
    }

    /**
     * Return the object id for <code>ob</code> if it has one, or
     * <code>null</code> otherwise.
     */
    private static Object getObjectId(Object ob) {
        if (!ImplHelper.isManageable(ob))
            return null;

        PersistenceCapable pc = ImplHelper.toPersistenceCapable(ob, null);
        if (pc == null || pc.pcIsNew())
            return null;
        else
            return pc.pcFetchObjectId();
	}

    public static String toClassName(Class<?> cls) {
        if (cls == null) return "";
        if (cls.isArray())
            return toClassName(cls.getComponentType())+"[]";
        return cls.getName();
    }

    public static String toClassNames(Collection<? extends Class<?>> classes) {
        if (classes == null) return "";
        StringBuilder buffer = new StringBuilder();
        for (Class<?> cls : classes) {
            buffer.append("\r\n").append(toClassName(cls));
        }
        return buffer.toString();
    }
}
