/*
* Copyright 2014 Grow Bit
*
* Licensed 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.turbogwt.net.serialization.client;
import java.util.Collections;
import org.turbogwt.core.collections.client.JsArrayList;
import org.turbogwt.core.collections.client.JsMap;
import org.turbogwt.core.util.shared.Registration;
/**
* Manager for registering and retrieving Serializers and Deserializers.
*
* @author Danilo Reinert
*/
public class SerdesManager {
private final JsMap<JsArrayList<DeserializerHolder>> deserializers = JsMap.create();
private final JsMap<JsArrayList<SerializerHolder>> serializers = JsMap.create();
/**
* Register a deserializer of the given type.
*
* @param type The class of the deserializer's type.
* @param deserializer The deserializer of T.
* @param <T> The type of the object to be deserialized.
*
* @return The {@link Registration} object, capable of cancelling this registration
* to the {@link SerdesManager}.
*/
public <T> Registration registerDeserializer(Class<T> type, Deserializer<T> deserializer) {
final String typeName = type.getName();
JsArrayList<DeserializerHolder> tDesList = deserializers.get(typeName);
if (tDesList == null) {
tDesList = new JsArrayList<>();
deserializers.put(typeName, tDesList);
}
final String[] accept = deserializer.accept();
final DeserializerHolder[] holders = new DeserializerHolder[accept.length];
for (int i = 0; i < accept.length; i++) {
String pattern = accept[i];
final Key key = new Key(type, pattern);
final DeserializerHolder holder = new DeserializerHolder(key, deserializer);
tDesList.add(holder);
holders[i] = holder;
}
Collections.sort(tDesList);
return new Registration() {
@Override
public void removeHandler() {
for (DeserializerHolder holder : holders) {
deserializers.get(typeName).remove(holder);
}
}
};
}
/**
* Register a serializer of the given type.
*
* @param type The class of the serializer's type.
* @param serializer The serializer of T.
* @param <T> The type of the object to be serialized.
*
* @return The {@link Registration} object, capable of cancelling this registration
* to the {@link SerdesManager}.
*/
public <T> Registration registerSerializer(Class<T> type, Serializer<T> serializer) {
final String typeName = type.getName();
JsArrayList<SerializerHolder> tSerList = serializers.get(typeName);
if (tSerList == null) {
tSerList = new JsArrayList<>();
serializers.put(typeName, tSerList);
}
final String[] contentType = serializer.contentType();
final SerializerHolder[] holders = new SerializerHolder[contentType.length];
for (int i = 0; i < contentType.length; i++) {
String pattern = contentType[i];
final Key key = new Key(type, pattern);
final SerializerHolder holder = new SerializerHolder(key, serializer);
tSerList.add(holder);
holders[i] = holder;
}
Collections.sort(tSerList);
return new Registration() {
@Override
public void removeHandler() {
for (SerializerHolder holder : holders) {
serializers.get(typeName).remove(holder);
}
}
};
}
/**
* Register a serializer/deserializer of the given type.
*
* @param type The class of the serializer/deserializer's type.
* @param serdes The serializer/deserializer of T.
* @param <T> The type of the object to be serialized/deserialized.
*
* @return The {@link Registration} object, capable of cancelling this registration
* to the {@link SerdesManager}.
*/
public <T> Registration registerSerdes(Class<T> type, Serdes<T> serdes) {
final Registration desReg = registerDeserializer(type, serdes);
final Registration serReg = registerSerializer(type, serdes);
return new Registration() {
@Override
public void removeHandler() {
desReg.removeHandler();
serReg.removeHandler();
}
};
}
/**
* Retrieve Deserializer from manager.
*
* @param type The type class of the deserializer.
* @param <T> The type of the deserializer.
* @return The deserializer of the specified type.
*
* @throws SerializationException if no deserializer was registered for the class.
*/
@SuppressWarnings("unchecked")
public <T> Deserializer<T> getDeserializer(Class<T> type, String contentType) throws SerializationException {
checkNotNull(type, "Type (Class<T>) cannot be null.");
checkNotNull(contentType, "Content-Type cannot be null.");
final Key key = new Key(type, contentType);
JsArrayList<DeserializerHolder> holders = deserializers.get(type.getName());
if (holders != null) {
for (DeserializerHolder holder : holders) {
if (holder.key.matches(key)) return (Deserializer<T>) holder.deserializer;
}
}
throw new SerializationException("There is no Deserializer registered for " + type.getName() +
" and content-type " + contentType + ".");
}
/**
* Retrieve Serializer from manager.
*
* @param type The type class of the serializer.
* @param <T> The type of the serializer.
* @return The serializer of the specified type.
* @throws SerializationException if no serializer was registered for the class.
*/
@SuppressWarnings("unchecked")
public <T> Serializer<T> getSerializer(Class<T> type, String contentType) throws SerializationException {
checkNotNull(type, "Type (Class<T>) cannot be null.");
checkNotNull(contentType, "Content-Type cannot be null.");
final Key key = new Key(type, contentType);
JsArrayList<SerializerHolder> holders = serializers.get(type.getName());
if (holders != null) {
for (SerializerHolder holder : holders) {
if (holder.key.matches(key)) return (Serializer<T>) holder.serializer;
}
}
throw new SerializationException("There is no Serializer registered for type " + type.getName() +
" and content-type " + contentType + ".");
}
private void checkNotNull(Object o, String message) {
if (o == null) throw new NullPointerException(message);
}
private static class DeserializerHolder implements Comparable<DeserializerHolder> {
final Key key;
final Deserializer<?> deserializer;
private DeserializerHolder(Key key, Deserializer<?> deserializer) {
this.key = key;
this.deserializer = deserializer;
}
@Override
public int compareTo(DeserializerHolder deserializerHolder) {
return key.compareTo(deserializerHolder.key);
}
@Override
public boolean equals(Object o) {
final DeserializerHolder that = (DeserializerHolder) o;
return key.equals(that.key);
}
@Override
public int hashCode() {
return key.hashCode();
}
}
private static class SerializerHolder implements Comparable<SerializerHolder> {
final Key key;
final Serializer<?> serializer;
private SerializerHolder(Key key, Serializer<?> serializer) {
this.key = key;
this.serializer = serializer;
}
@Override
public int compareTo(SerializerHolder serializerHolder) {
return key.compareTo(serializerHolder.key);
}
@Override
public boolean equals(Object o) {
final SerializerHolder that = (SerializerHolder) o;
return key.equals(that.key);
}
@Override
public int hashCode() {
return key.hashCode();
}
}
private static class Key implements Comparable<Key> {
final Class<?> type;
final String contentType;
final double factor;
private Key(Class<?> type, String contentType) {
checkSeparatorPresence(contentType);
this.type = type;
this.contentType = contentType;
this.factor = 1.0;
}
private Key(Class<?> type, String contentType, double factor) {
this.type = type;
this.contentType = contentType;
this.factor = factor;
}
// TODO: test exhaustively
public boolean matches(Key key) {
if (!key.type.equals(this.type)) {
return false;
}
boolean matches;
final int thisSep = this.contentType.indexOf("/");
final int otherSep = key.contentType.indexOf("/");
String thisInitialPart = this.contentType.substring(0, thisSep);
String otherInitialPart = key.contentType.substring(0, otherSep);
if (thisInitialPart.contains("*")) {
matches = matchPartsSafely(thisInitialPart, otherInitialPart);
} else if (otherInitialPart.contains("*")) {
matches = matchPartsUnsafely(otherInitialPart, thisInitialPart);
} else {
matches = thisInitialPart.equalsIgnoreCase(otherInitialPart);
}
if (!matches) return false;
final String thisFinalPart = this.contentType.substring(thisSep + 1);
final String otherFinalPart = key.contentType.substring(otherSep + 1);
if (thisFinalPart.contains("*")) {
matches = matchPartsSafely(thisFinalPart, otherFinalPart);
} else if (otherFinalPart.contains("*")) {
matches = matchPartsUnsafely(otherFinalPart, thisFinalPart);
} else {
matches = thisFinalPart.equalsIgnoreCase(otherFinalPart);
}
return matches;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Key)) {
return false;
}
final Key key = (Key) o;
if (!type.equals(key.type)) {
return false;
}
if (!contentType.equals(key.contentType)) {
return false;
}
if (Double.compare(key.factor, factor) != 0) {
return false;
}
return true;
}
@Override
public int hashCode() {
int result;
long temp;
result = type.hashCode();
result = 31 * result + contentType.hashCode();
temp = Double.doubleToLongBits(factor);
result = 31 * result + (int) (temp ^ (temp >>> 32));
return result;
}
@Override
public int compareTo(Key key) {
int result = this.type.getSimpleName().compareTo(key.type.getSimpleName());
// TODO: Improve pattern matching to handle patterns without separators.
if (result == 0) {
final int thisSep = this.contentType.indexOf("/");
final int otherSep = key.contentType.indexOf("/");
// !!! CAUTION !!!
// When contentType does not have a '/' separator, than StringArrayIndexOutOfBounds is thrown.
String thisInitialPart = this.contentType.substring(0, thisSep);
String otherInitialPart = key.contentType.substring(0, otherSep);
result = thisInitialPart.compareTo(otherInitialPart);
// Invert the result if the winner contains wildcard
if ((result < 0 && thisInitialPart.contains("*")) || (result > 0 && otherInitialPart.contains("*")))
result = -result;
if (result == 0) {
String thisFinalPart = this.contentType.substring(thisSep + 1);
String otherFinalPart = key.contentType.substring(otherSep + 1);
result = thisFinalPart.compareTo(otherFinalPart);
// Invert the result if the winner contains wildcard
if ((result < 0 && thisFinalPart.contains("*")) || (result > 0 && otherFinalPart.contains("*")))
result = -result;
if (result == 0) {
// Invert comparison because the greater the factor the greater the precedence.
result = Double.compare(key.factor, this.factor);
}
}
}
return result;
}
private void checkSeparatorPresence(String contentType) {
if (contentType.indexOf("/") < 1)
throw new RuntimeException("Cannot perform matching. Content-Type *" +
this.contentType + "* does not have a '/' separator.");
}
private boolean matchPartsSafely(String left, String right) {
boolean matches = true;
final String rightCleaned = right.replace("*", "").toLowerCase();
String[] parts = left.toLowerCase().split("\\*");
final boolean otherEndsWithWildcard = right.endsWith("*");
final int otherCleanedLength = rightCleaned.length();
int i = 0;
for (String part : parts) {
if (i == otherCleanedLength && otherEndsWithWildcard) {
break;
}
if (!part.isEmpty()) {
int newIdx = rightCleaned.indexOf(part, i);
if (newIdx == -1) {
matches = false;
break;
}
i = newIdx + part.length();
}
}
return matches;
}
private boolean matchPartsUnsafely(String left, String right) {
boolean matches = true;
String rightLower = right.toLowerCase();
String[] parts = left.toLowerCase().split("\\*");
int i = 0;
for (String part : parts) {
if (!part.isEmpty()) {
int newIdx = rightLower.indexOf(part, i);
if (newIdx == -1) {
matches = false;
break;
}
i = newIdx + part.length();
}
}
return matches;
}
@Override
public String toString() {
return "{" +
"type: '" + type.getName() + '\'' +
", contentType: '" + contentType + '\'' +
", factor: " + factor +
'}';
}
}
}