001/*-------------------------------------------------------------------------+
002|                                                                          |
003| Copyright 2005-2011 the ConQAT Project                                   |
004|                                                                          |
005| Licensed under the Apache License, Version 2.0 (the "License");          |
006| you may not use this file except in compliance with the License.         |
007| 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.conqat.lib.commons.serialization;
018
019import java.io.ByteArrayOutputStream;
020import java.io.DataOutputStream;
021import java.io.IOException;
022import java.io.ObjectStreamConstants;
023import java.io.OutputStream;
024import java.util.HashMap;
025import java.util.IdentityHashMap;
026import java.util.List;
027import java.util.Map;
028
029import org.conqat.lib.commons.assertion.CCSMAssert;
030import org.conqat.lib.commons.serialization.objects.LongStringUtils;
031import org.conqat.lib.commons.serialization.objects.SerializedObjectBase;
032import org.conqat.lib.commons.serialization.objects.SerializedStringObject;
033
034/**
035 * Serializes serialized entities (objects + classes) back to a stream.
036 */
037public class SerializedEntitySerializer {
038
039        /** Maximal allowed size for short block data. */
040        private static final int MAX_SHORT_BLOCK_DATA_SIZE = 255;
041
042        /** The stream to write into. */
043        private final DataOutputStream dos;
044
045        /** Counter used to determine the next entity handle. */
046        private int nextHandle = SerializedEntityPool.START_HANDLE;
047
048        /** Entities already written together with their handles. */
049        private final Map<SerializedEntityBase, Integer> entityPool = new IdentityHashMap<>();
050
051        /**
052         * Strings already written together with their handles. This is kept
053         * separate from the {@link #entityPool} to allow exploitation of
054         * (accidental) equality in strings.
055         */
056        private final Map<String, Integer> stringPool = new HashMap<>();
057
058        /** Constructor. */
059        private SerializedEntitySerializer(DataOutputStream dos) {
060                this.dos = dos;
061        }
062
063        /** Performs the actual serialization. */
064        private void serialize(List<SerializedEntityBase> entities) throws IOException {
065                dos.writeShort(ObjectStreamConstants.STREAM_MAGIC);
066                dos.writeShort(ObjectStreamConstants.STREAM_VERSION);
067
068                for (SerializedEntityBase entity : entities) {
069                        if (entity == null) {
070                                dos.writeByte(ObjectStreamConstants.TC_NULL);
071                        } else {
072                                entity.serialize(dos, this);
073                        }
074                }
075        }
076
077        /** Serializes a pool to a data output stream. */
078        public static void serializeToStream(List<SerializedEntityBase> entities, DataOutputStream dos) throws IOException {
079                new SerializedEntitySerializer(dos).serialize(entities);
080        }
081
082        /** Serializes a pool to an output stream. */
083        public static void serializeToStream(List<SerializedEntityBase> entities, OutputStream out) throws IOException {
084                DataOutputStream dos = new DataOutputStream(out);
085                serializeToStream(entities, dos);
086                dos.flush();
087        }
088
089        /** Serializes a pool to a raw array. */
090        public static byte[] serializeToBytes(List<SerializedEntityBase> entities) throws IOException {
091                ByteArrayOutputStream baos = new ByteArrayOutputStream();
092                serializeToStream(entities, baos);
093                baos.close();
094                return baos.toByteArray();
095        }
096
097        /** Serializes a string as an object. */
098        public void serializeStringObject(String value) throws IOException {
099                serializeStringObject(value, true);
100        }
101
102        /** Serializes a string as an object. */
103        public void serializeStringObject(String value, boolean useStringPool) throws IOException {
104                if (useStringPool) {
105                        Integer existingHandle = stringPool.get(value);
106                        if (existingHandle != null) {
107                                dos.writeByte(ObjectStreamConstants.TC_REFERENCE);
108                                dos.writeInt(existingHandle);
109                                return;
110                        }
111                }
112
113                stringPool.put(value, obtainHandle());
114                if (value.length() <= LongStringUtils.MAX_SHORT_STRING_LENGTH) {
115                        dos.writeByte(ObjectStreamConstants.TC_STRING);
116                        dos.writeUTF(value);
117                } else {
118                        dos.writeByte(ObjectStreamConstants.TC_LONGSTRING);
119                        LongStringUtils.writeUTF(value, dos);
120                }
121        }
122
123        /** Writes a raw block of data. */
124        public void writeBlockData(byte[] data) throws IOException {
125                if (data.length <= MAX_SHORT_BLOCK_DATA_SIZE) {
126                        dos.write(ObjectStreamConstants.TC_BLOCKDATA);
127                        dos.writeByte(data.length);
128                } else {
129                        dos.write(ObjectStreamConstants.TC_BLOCKDATALONG);
130                        dos.writeInt(data.length);
131                }
132                dos.write(data);
133        }
134
135        /**
136         * Serializes an annotation list as found in <code>objectAnnotation</code>
137         * and <code>classAnnotation</code>. Note that this has nothing to do with
138         * Java annotations (the language feature), but rather is a list consisting
139         * of raw data (byte arrays) and handles (int).
140         */
141        public void serializeAnnotationList(List<Object> rawDataList, SerializedEntityPool pool)
142                        throws IOException {
143                for (Object rawData : rawDataList) {
144                        if (rawData instanceof byte[]) {
145                                writeBlockData((byte[]) rawData);
146                        } else if (rawData instanceof Integer) {
147                                int handle = (Integer) rawData;
148                                serializeObject(handle, SerializedObjectBase.class, pool, dos, this);
149                        } else {
150                                throw new SerializationConsistencyException("Unexpected type in class annotations!");
151                        }
152                }
153        }
154
155        /**
156         * Serializes the object defined by the given handle. Also accepts the null
157         * handle.
158         */
159        public static void serializeObject(int handle, Class<? extends SerializedEntityBase> expectedType,
160                        SerializedEntityPool pool, DataOutputStream dos, SerializedEntitySerializer serializer) throws IOException {
161                if (handle == SerializedEntityPool.NULL_HANDLE) {
162                        dos.writeByte(ObjectStreamConstants.TC_NULL);
163                } else {
164                        pool.getEntity(handle, expectedType).serialize(dos, serializer);
165                }
166        }
167
168        /** Returns the next handle for a written object. */
169        private int obtainHandle() {
170                return nextHandle++;
171        }
172
173        /**
174         * Attempts to write a reference to this object if it has been written
175         * before. Returns whether a reference could be written. This may not be
176         * used for strings ({@link SerializedStringObject}).
177         */
178        public boolean writeReference(SerializedEntityBase entity) throws IOException {
179                CCSMAssert.isFalse(entity instanceof SerializedStringObject, "String are handled in a separate pool!");
180
181                Integer existingHandle = entityPool.get(entity);
182                if (existingHandle != null) {
183                        dos.writeByte(ObjectStreamConstants.TC_REFERENCE);
184                        dos.writeInt(existingHandle);
185                        return true;
186                }
187                return false;
188        }
189
190        /** Registers an entity for a handle to be referenceable later on. */
191        public void registerHandle(SerializedEntityBase entity) throws IOException {
192                if (entityPool.containsKey(entity)) {
193                        throw new IOException("Duplicate registration of entity " + entity);
194                }
195
196                entityPool.put(entity, obtainHandle());
197        }
198
199}