/*
 * 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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.io.OutputStream;
import java.io.Serializable;
import java.security.AccessController;
import java.security.PrivilegedAction;

import org.apache.openjpa.conf.OpenJPAConfiguration;
import org.apache.openjpa.kernel.StoreContext;
import org.apache.openjpa.lib.log.Log;
import org.apache.openjpa.lib.util.J2DoPrivHelper;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.MultiClassLoader;

/**
 * Helper class to serialize and deserialize persistent objects,
 * subtituting oids into the serialized stream and subtituting the persistent
 * objects back during deserialization.
 *
 * @author Abe White
 * @since 0.3.3
 */
public class Serialization {

    private static final Localizer _loc = Localizer.forPackage
        (Serialization.class);

    /**
     * Serialize a value that might contain persistent objects. Replaces
     * persistent objects with their oids.
     */
    public static byte[] serialize(Object val, StoreContext ctx) {
        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
        try {
            ObjectOutputStream objs = new PersistentObjectOutputStream(bytes,
                ctx);
            objs.writeObject(val);
            objs.flush();
            return bytes.toByteArray();
        } catch (Exception e) {
            throw new StoreException(e);
        }
    }

    /**
     * Deserialize an object value from the given bytes.
     */
    public static Object deserialize(byte[] bytes, StoreContext ctx) {
        ByteArrayInputStream in = new ByteArrayInputStream(bytes);
        return deserialize(in, ctx);
    }

    /**
     * Deserialize an object value from the given stream.
     */
    public static Object deserialize(InputStream in, StoreContext ctx) {
        try {
            if (ctx == null)
                return new ClassResolvingObjectInputStream(in).readObject();
            return new PersistentObjectInputStream(in, ctx).readObject();
        } catch (Exception e) {
            throw new StoreException(e);
        }
    }

    /**
     * Object output stream that replaces persistent objects with their oids.
     */
    public static class PersistentObjectOutputStream
        extends ObjectOutputStream {

        private StoreContext _ctx;

        /**
         * Constructor; supply underlying stream.
         */
        public PersistentObjectOutputStream(OutputStream delegate,
            StoreContext ctx)
            throws IOException {
            super(delegate);
            _ctx = ctx;
            AccessController.doPrivileged(new PrivilegedAction() {
                @Override
                public Object run() {
                    enableReplaceObject(true);
                    return null;
                }
            });
        }

        @Override
        protected Object replaceObject(Object obj) {
            Object oid = _ctx.getObjectId(obj);
            return (oid == null) ? obj : new ObjectIdMarker(oid);
        }
    }

    public static class ClassResolvingObjectInputStream
        extends ObjectInputStream {

        public ClassResolvingObjectInputStream(InputStream delegate)
            throws IOException {
            super(delegate);
        }

        @Override
        protected Class resolveClass(ObjectStreamClass desc)
            throws IOException, ClassNotFoundException {
            String name = BlacklistClassResolver.DEFAULT.check(desc.getName());
            MultiClassLoader loader = AccessController
                .doPrivileged(J2DoPrivHelper.newMultiClassLoaderAction());
            addContextClassLoaders(loader);
            loader.addClassLoader(getClass().getClassLoader());
            loader.addClassLoader(MultiClassLoader.SYSTEM_LOADER);
            return Class.forName(name, true, loader);
        }

        protected void addContextClassLoaders(MultiClassLoader loader) {
            loader.addClassLoader(AccessController.doPrivileged(
                J2DoPrivHelper.getContextClassLoaderAction()));
        }
    }

    /**
     * Object input stream that replaces oids with their objects.
     */
    public static class PersistentObjectInputStream
        extends ClassResolvingObjectInputStream {

        private final StoreContext _ctx;

        /**
         * Constructor; supply source stream and broker to
         * use for persistent object lookups.
         */
        public PersistentObjectInputStream(InputStream delegate,
            StoreContext ctx)
            throws IOException {
            super(delegate);
            _ctx = ctx;
            AccessController.doPrivileged(new PrivilegedAction() {
                @Override
                public Object run() {
                    enableResolveObject(true);
                    return null;
                }
            });
        }

        @Override
        protected void addContextClassLoaders(MultiClassLoader loader) {
            super.addContextClassLoaders(loader);
            loader.addClassLoader(_ctx.getClassLoader());
        }

        @Override
        protected Object resolveObject(Object obj) {
            if (!(obj instanceof ObjectIdMarker))
                return obj;

            Object oid = ((ObjectIdMarker) obj).oid;
            if (oid == null)
                return null;

            Object pc = _ctx.find(oid, null, null, null, 0);
            if (pc == null) {
                Log log = _ctx.getConfiguration().getLog
                    (OpenJPAConfiguration.LOG_RUNTIME);
                if (log.isWarnEnabled())
                    log.warn(_loc.get("bad-ser-oid", oid));
                if (log.isTraceEnabled())
                    log.trace(new ObjectNotFoundException(oid));
            }
            return pc;
        }
    }

    /**
     * Marker for oids.
     */
    private static class ObjectIdMarker
        implements Serializable {

        
        private static final long serialVersionUID = 1L;
        public Object oid;

        public ObjectIdMarker(Object oid) {
            this.oid = oid;
		}
	}
}

