org.apache.cayenne.lifecycle.id.EntityIdCoder Maven / Gradle / Ivy
The newest version!
/*****************************************************************
* 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
*
* https://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.cayenne.lifecycle.id;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.SortedMap;
import java.util.StringTokenizer;
import java.util.TreeMap;
import org.apache.cayenne.CayenneRuntimeException;
import org.apache.cayenne.ObjectId;
import org.apache.cayenne.dba.TypesMapping;
import org.apache.cayenne.map.DbAttribute;
import org.apache.cayenne.map.ObjAttribute;
import org.apache.cayenne.map.ObjEntity;
import org.apache.cayenne.util.IDUtil;
import org.apache.cayenne.util.Util;
/**
* An object to encode/decode ObjectIds for a single mapped entity.
*
* @since 3.1
*/
public class EntityIdCoder {
static final String ID_SEPARATOR = ":";
static final String TEMP_ID_PREFIX = ".";
private static final int TEMP_PREFIX_LENGTH = TEMP_ID_PREFIX.length();
private String entityName;
private SortedMap> converters;
private int idSize;
public static String getEntityName(String id) {
int separator = id.indexOf(ID_SEPARATOR);
if (separator <= 0 || separator == id.length() - 1) {
throw new IllegalArgumentException("Invalid String id: " + id);
}
String name = id.substring(0, separator);
if (name.startsWith(TEMP_ID_PREFIX)) {
name = name.substring(TEMP_PREFIX_LENGTH);
}
return name;
}
public EntityIdCoder(ObjEntity entity) {
this.entityName = entity.getName();
this.converters = new TreeMap<>();
for (ObjAttribute attribute : entity.getAttributes()) {
if (attribute.isPrimaryKey()) {
converters.put(attribute.getDbAttributeName(), create(attribute.getJavaClass()));
}
}
for (DbAttribute attribute : entity.getDbEntity().getPrimaryKeys()) {
if (!converters.containsKey(attribute.getName())) {
String type = TypesMapping.getJavaBySqlType(attribute);
try {
converters.put(attribute.getName(), create(Util.getJavaClass(type)));
} catch (ClassNotFoundException e) {
throw new CayenneRuntimeException("Can't instantiate class " + type, e);
}
}
}
if (converters.isEmpty()) {
throw new IllegalArgumentException("Entity has no PK defined: " + entity.getName());
}
this.idSize = (int) Math.ceil(converters.size() / 0.75d);
}
/**
* Returns a consistent String representation of the ObjectId
*/
public String toStringId(ObjectId id) {
// deal with temp that have attached replacement ID as permanent IDs...
// AuditableFilter, etc. all rely on the ability to find the temp object
// after the transaction end
// TODO: support encoding format for temp+replacement
if (id.isTemporary() && !id.isReplacementIdAttached()) {
return toTempIdString(id);
} else {
return toPermIdString(id);
}
}
private String toTempIdString(ObjectId id) {
StringBuilder buffer = new StringBuilder();
buffer.append(TEMP_ID_PREFIX);
buffer.append(id.getEntityName());
buffer.append(ID_SEPARATOR);
for (byte b : id.getKey()) {
IDUtil.appendFormattedByte(buffer, b);
}
return buffer.toString();
}
private String toPermIdString(ObjectId id) {
StringBuilder buffer = new StringBuilder();
buffer.append(id.getEntityName());
Map idValues = id.getIdSnapshot();
for (Entry> entry : converters.entrySet()) {
Object value = idValues.get(entry.getKey());
buffer.append(ID_SEPARATOR).append(entry.getValue().toUuid(value));
}
return buffer.toString();
}
public ObjectId toObjectId(String stringId) {
if (stringId.startsWith(TEMP_ID_PREFIX)) {
String idValues = stringId.substring(entityName.length() + 1 + TEMP_PREFIX_LENGTH);
return ObjectId.of(entityName, decodeTemp(idValues));
}
String idValues = stringId.substring(entityName.length() + 1);
if (converters.size() == 1) {
Entry> entry = converters.entrySet().iterator().next();
String decoded;
try {
decoded = URLDecoder.decode(idValues, "UTF-8");
} catch (UnsupportedEncodingException e) {
// unexpected
throw new CayenneRuntimeException("Unsupported encoding", e);
}
return ObjectId.of(entityName, entry.getKey(), entry.getValue().fromStringId(decoded));
}
Map idMap = new HashMap<>(idSize);
StringTokenizer toks = new StringTokenizer(idValues, ID_SEPARATOR);
if (toks.countTokens() != converters.size()) {
throw new IllegalArgumentException("Invalid String ID for entity "
+ entityName + ": " + idValues);
}
for (Entry> entry : converters.entrySet()) {
String value = toks.nextToken();
String decoded;
try {
decoded = URLDecoder.decode(value, "UTF-8");
} catch (UnsupportedEncodingException e) {
// unexpected
throw new CayenneRuntimeException("Unsupported encoding", e);
}
idMap.put(entry.getKey(), entry.getValue().fromStringId(decoded));
}
return ObjectId.of(entityName, idMap);
}
private byte[] decodeTemp(String byteString) {
byte[] bytes = new byte[byteString.length() / 2];
for (int i = 0; i < bytes.length; i++) {
int index = i * 2;
// this is better than Byte.parseByte which can't parse values >=
// 128 as negative bytes
int c1 = byteString.charAt(index);
int c2 = byteString.charAt(index + 1);
bytes[i] = (byte) ((Character.digit(c1, 16) << 4) + Character.digit(c2, 16));
}
return bytes;
}
private Converter> create(Class> type) {
if (type == null) {
throw new NullPointerException("Null type");
}
if (Long.class.isAssignableFrom(type)) {
return Long::valueOf;
} else if (Integer.class.isAssignableFrom(type)) {
return Integer::valueOf;
} else if (String.class.isAssignableFrom(type)) {
return stringId -> stringId;
}
throw new IllegalArgumentException("Unsupported ID type: " + type.getName());
}
interface Converter {
default String toUuid(Object value) {
try {
return URLEncoder.encode(String.valueOf(value), "UTF-8");
} catch (UnsupportedEncodingException e) {
// unexpected
throw new CayenneRuntimeException("Unsupported encoding", e);
}
}
T fromStringId(String stringId);
}
}