prompto.server.StoreServlet Maven / Gradle / Ivy
The newest version!
package prompto.server;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import javax.servlet.MultipartConfigElement;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import prompto.intrinsic.PromptoDate;
import prompto.intrinsic.PromptoDateTime;
import prompto.intrinsic.PromptoDbId;
import prompto.intrinsic.PromptoTime;
import prompto.intrinsic.PromptoVersion;
import prompto.store.AttributeInfo;
import prompto.store.DataStore;
import prompto.store.IAuditMetadata;
import prompto.store.IQueryBuilder;
import prompto.store.IQueryBuilder.MatchOp;
import prompto.store.IStorable;
import prompto.store.IStorable.IDbIdFactory;
import prompto.store.IStore;
import prompto.store.IStored;
import prompto.store.IStoredIterable;
import prompto.type.BinaryType;
import prompto.utils.Logger;
@SuppressWarnings("serial")
public class StoreServlet extends CleverServlet {
static Logger logger = new Logger();
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
setMultipartConfig(new MultipartConfigElement(System.getProperty("java.io.tmpdir")));
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Thread.currentThread().setName(this.getClass().getSimpleName());
doStuff(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Thread.currentThread().setName(this.getClass().getSimpleName());
doStuff(req, resp);
}
protected void doStuff(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String path = req.getPathInfo();
if(path!=null) switch(path) {
case "/fetchOne":
fetchOne(req, resp);
break;
case "/fetchMany":
fetchMany(req, resp);
break;
case "/deleteAndStore":
deleteAndStore(req, resp);
break;
default:
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
}
}
protected void deleteAndStore(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try(var output = resp.getOutputStream()) {
try {
String contentType = req.getContentType();
if(contentType.startsWith("application/json"))
deleteAndStoreJson(req, resp, output);
else if(contentType.startsWith("application/x-www-form-urlencoded"))
deleteAndStoreUrlEncoded(req, resp, output);
else if(contentType.startsWith("multipart/form-data"))
deleteAndStoreMultipart(req, resp, output);
else
resp.sendError(415);
} catch(Throwable t) {
t.printStackTrace();
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
writeJSONError(t.getMessage(), output);
}
}
}
private void deleteAndStoreJson(HttpServletRequest req, HttpServletResponse resp, OutputStream output) throws IOException {
Map updatedDbIds = new HashMap<>();
JsonNode json = readJsonStream(req);
JsonNode toDelete = null;
JsonNode toStore = null;
if(json.has("toDelete"))
toDelete = json.get("toDelete");
if(json.has("toStore"))
toStore = json.get("toStore");
if(toDelete!=null || toStore!=null)
updatedDbIds = deleteAndStoreJson(toDelete, toStore, null, json.get("withMeta"));
writeJSONResult(updatedDbIds, output);
}
private Map deleteAndStoreJson(JsonNode toDelete, JsonNode toStore, Map parts, JsonNode withMeta) {
Collection deletables = collectDeletables(toDelete);
Map updatedDbIds = new HashMap<>();
List storables = collectStorables(toStore, parts, updatedDbIds);
IAuditMetadata metadata = collectMetadata(withMeta, parts);
DataStore.getInstance().deleteAndStore(deletables, storables, metadata);
return updatedDbIds;
}
private Collection collectDeletables(JsonNode toDelete) {
if(toDelete==null)
return null;
else
return jsonToDbIdsCollection(toDelete);
}
@SuppressWarnings("unchecked")
private Collection jsonToDbIdsCollection(JsonNode node) {
Object converted = convertJsonToDbIds(node);
if(converted==null)
return Collections.emptyList();
else if(converted instanceof Collection)
return (Collection)converted;
else if(converted instanceof PromptoDbId)
return Collections.singletonList((PromptoDbId)converted);
else
throw new UnsupportedOperationException(converted.getClass().getName());
}
private Object convertJsonToDbIds(JsonNode node) {
if(node.isNumber())
return convertJsonToDbId(node.asLong());
else if(node.isTextual())
return convertJsonToDbId(node.asText());
else if(node.isArray()) {
return StreamSupport.stream(node.spliterator(), false)
.map(this::convertJsonToDbIds)
.collect(Collectors.toList());
} else {
logger.error(()->"Could not convert: " + node.toString());
return null;
}
}
private Object convertJsonToDbId(Object dbId) {
return DataStore.getInstance().convertToDbId(dbId);
}
private List collectStorables(JsonNode toStore, Map parts, Map updatedDbIds) {
if(toStore==null)
return null;
else if(toStore.isArray())
return StreamSupport.stream(toStore.spliterator(), false)
.map(node->jsonToStorable(node, parts, updatedDbIds))
.collect(Collectors.toList());
else {
logger.error(()->"Could not store: " + toStore.toString());
return null;
}
}
private IStorable jsonToStorable(JsonNode record, Map parts, Map updatedDbIds) {
if(isRecordUpdate(record))
return populateExistingStorable(record, parts, updatedDbIds);
else
return populateNewStorable(record, parts, updatedDbIds);
}
private boolean isRecordUpdate(JsonNode record) {
JsonNode dbIdField = record.get(IStore.dbIdName);
return dbIdField!=null && !dbIdField.has("tempDbId");
}
private IStorable populateExistingStorable(JsonNode record, Map parts, Map updatedDbIds) {
// use dbId received from client
Object rawDbId = readJsonValue(record.get(IStore.dbIdName), parts, updatedDbIds);
PromptoDbId dbId = DataStore.getInstance().convertToDbId(rawDbId);
// dbId factory
IDbIdFactory dbIdFactory = new IDbIdFactory() {
@Override public void accept(PromptoDbId dbId) { throw new IllegalStateException(); }
@Override public PromptoDbId get() { return dbId; }
@Override public boolean isUpdate() { return true; }
};
// populate storable
List categories = readJsonCategories(record);
IStorable storable = DataStore.getInstance().newStorable(categories, dbIdFactory);
Iterator fieldNames = record.fieldNames();
while(fieldNames.hasNext()) {
String fieldName = fieldNames.next();
if(IStore.dbIdName.equals(fieldName))
continue;
Object value = readJsonValue(record.get(fieldName), parts, updatedDbIds);
storable.setData(fieldName, value);
}
return storable;
}
private IStorable populateNewStorable(JsonNode record, Map parts, Map updatedDbIds) {
// use potentially existing dbId allocated by a dbRef in another storable
Long tempDbId = record.get(IStore.dbIdName).get("tempDbId").asLong();
PromptoDbId dbId = DataStore.getInstance().convertToDbId(updatedDbIds.getOrDefault(tempDbId, null));
// dbId factory
IDbIdFactory dbIdFactory = new IDbIdFactory() {
@Override public void accept(PromptoDbId newDbId) { updatedDbIds.put(tempDbId, newDbId); }
@Override public PromptoDbId get() { return dbId; }
@Override public boolean isUpdate() { return false; }
};
// populate storable
List categories = readJsonCategories(record);
IStorable storable = DataStore.getInstance().newStorable(categories, dbIdFactory );
Iterator fieldNames = record.fieldNames();
while(fieldNames.hasNext()) {
String fieldName = fieldNames.next();
if(IStore.dbIdName.equals(fieldName))
continue;
else {
Object value = readJsonValue(record.get(fieldName), parts, updatedDbIds);
storable.setData(fieldName, value);
}
}
return storable;
}
private Object readJsonValue(JsonNode fieldValue, Map parts, Map updatedDbIds) {
if(fieldValue.has("tempDbId"))
return updatedDbIds.computeIfAbsent(fieldValue.get("tempDbId").asLong(), k->DataStore.getInstance().newDbId());
else if(fieldValue.isDouble() || fieldValue.isFloat())
return fieldValue.asDouble();
else if(fieldValue.isLong() || fieldValue.isInt())
return fieldValue.asLong();
else if(fieldValue.isBoolean())
return fieldValue.asBoolean();
else if(fieldValue.isTextual())
return fieldValue.asText();
else if(fieldValue.isNull())
return null;
else if(fieldValue.isObject()) {
if(fieldValue.has("name"))
return fieldValue.get("name").asText();
else
return readJsonValue(fieldValue.get("type").asText(), fieldValue.get("value"), parts, updatedDbIds);
} else if(fieldValue.isArray())
return StreamSupport.stream(fieldValue.spliterator(), false)
.map(node->readJsonValue(node, parts, updatedDbIds))
.collect(Collectors.toList());
else
throw new UnsupportedOperationException(fieldValue.getNodeType().name());
}
private Object readJsonValue(String type, JsonNode fieldValue, Map parts, Map updatedDbIds) {
switch(type) {
case "Uuid":
return UUID.fromString(fieldValue.asText());
case "Date":
return PromptoDate.parse(fieldValue.asText());
case "Time":
return PromptoTime.parse(fieldValue.asText());
case "DateTime":
return PromptoDateTime.parse(fieldValue.asText());
case "Version":
return PromptoVersion.parse(fieldValue.asText());
case "Blob":
case "Image":
return BinaryType.readJSONValue(fieldValue, parts);
case "DbId":
case "%dbRef%":
return DataStore.getInstance().convertToNativeDbId(readJsonValue(fieldValue, parts, updatedDbIds));
default:
throw new UnsupportedOperationException(type);
}
}
private List readJsonCategories(JsonNode node) {
return StreamSupport.stream(node.get("category").spliterator(), false)
.map(JsonNode::asText)
.collect(Collectors.toList());
}
private IAuditMetadata collectMetadata(JsonNode withMeta, Map parts) {
if(withMeta!=null) {
if(withMeta.isObject()) {
if(DataStore.getInstance().isAuditEnabled()) {
IAuditMetadata metadata = DataStore.getInstance().newAuditMetadata();
withMeta.fields().forEachRemaining(e -> collectMetadata(metadata, e, parts));
return metadata;
} else
logger.error(()->"Could not store metadata because audit is disabled.");
} else
logger.error(()->"Could not convert: " + withMeta.toString());
}
return null;
}
private void collectMetadata(IAuditMetadata metadata, Entry entry, Map parts) {
metadata.put(entry.getKey(), readJsonValue(entry.getValue(), parts, Collections.emptyMap()));
}
private void deleteAndStoreMultipart(HttpServletRequest req, HttpServletResponse resp, OutputStream output) throws ServletException, IOException {
Map result = new HashMap<>();
Map parts = readPartsAsBytes(req);
JsonNode toDelete = null;
JsonNode toStore = null;
JsonNode withMeta = null;
if(parts.containsKey("toDelete"))
toDelete = new ObjectMapper().readTree(parts.get("toDelete"));
if(parts.containsKey("toStore"))
toStore = new ObjectMapper().readTree(parts.get("toStore"));
if(toDelete!=null || toStore!=null) {
if(parts.containsKey("withMeta"))
withMeta = new ObjectMapper().readTree(parts.get("withMeta"));
Map updatedDbIds = deleteAndStoreJson(toDelete, toStore, parts, withMeta);
updatedDbIds.forEach((k,v)->result.put(k, v.getValue()));
}
writeJSONResult(result, output);
}
private void deleteAndStoreUrlEncoded(HttpServletRequest req, HttpServletResponse resp, OutputStream output) throws IOException {
resp.sendError(415);
}
protected void fetchMany(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try(var output = resp.getOutputStream()) {
try {
String contentType = req.getContentType();
if(contentType.startsWith("application/json"))
fetchManyJson(req, resp, output);
else if(contentType.startsWith("application/x-www-form-urlencoded"))
fetchManyUrlEncoded(req, resp, output);
else if(contentType.startsWith("multipart/form-data"))
fetchManyMultipart(req, resp, output);
else
resp.sendError(415);
} catch(Throwable t) {
t.printStackTrace();
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
writeJSONError(t.getMessage(), output);
}
}
}
private void fetchManyMultipart(HttpServletRequest req, HttpServletResponse resp, OutputStream output) throws ServletException, IOException {
resp.sendError(415);
}
private void fetchManyUrlEncoded(HttpServletRequest req, HttpServletResponse resp, OutputStream output) throws IOException {
resp.sendError(415);
}
private void fetchManyJson(HttpServletRequest req, HttpServletResponse resp, OutputStream output) throws IOException {
JsonNode json = readJsonStream(req);
IQueryBuilder builder = DataStore.getInstance().newQueryBuilder();
if(json.has("predicate") && !json.get("predicate").isNull())
readPredicateJson(builder, json.get("predicate"));
if(json.has("first") && !json.get("first").isNull())
builder = builder.first(json.get("first").asLong());
if(json.has("last") && !json.get("last").isNull())
builder = builder.last(json.get("last").asLong());
if(json.has("projection") && json.get("projection").isArray())
readProjectionJson(builder, json.get("projection"));
if(json.has("orderBys") && !json.get("orderBys").isNull())
readOrderBysJson(builder, json.get("orderBys"));
resp.setContentType("application/json");
IStoredIterable fetched = DataStore.getInstance().fetchMany(builder.build());
JsonRecordsWriter writer = new JsonRecordsWriter(output, this::fetchAttributeInfo, DataStore.getInstance(), true);
writer.writeRecords(fetched);
}
protected void fetchOne(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try(var output = resp.getOutputStream()) {
try {
String contentType = req.getContentType();
if(contentType.startsWith("application/json"))
fetchOneJson(req, resp, output);
else if(contentType.startsWith("application/x-www-form-urlencoded"))
fetchOneUrlEncoded(req, resp, output);
else if(contentType.startsWith("multipart/form-data"))
fetchOneMultipart(req, resp, output);
else
resp.sendError(415);
} catch(Throwable t) {
t.printStackTrace();
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
writeJSONError(t.getMessage(), output);
}
}
}
private void fetchOneMultipart(HttpServletRequest req, HttpServletResponse resp, OutputStream output) throws IOException {
resp.sendError(415);
}
private void fetchOneUrlEncoded(HttpServletRequest req, HttpServletResponse resp, OutputStream output) throws IOException {
resp.sendError(415);
}
private void fetchOneJson(HttpServletRequest req, HttpServletResponse resp, OutputStream output) throws IOException {
JsonNode json = readJsonStream(req);
IQueryBuilder builder = DataStore.getInstance().newQueryBuilder();
if(json.has("predicate") && !json.get("predicate").isNull())
readPredicateJson(builder, json.get("predicate"));
if(json.has("projection") && json.get("projection").isArray())
readProjectionJson(builder, json.get("projection"));
resp.setContentType("application/json");
IStored fetched = DataStore.getInstance().fetchOne(builder.build());
JsonRecordsWriter writer = new JsonRecordsWriter(output, this::fetchAttributeInfo, DataStore.getInstance(), true);
writer.writeRecord(fetched);
}
private void readProjectionJson(IQueryBuilder builder, JsonNode jsonNode) {
List projection = StreamSupport.stream(jsonNode.spliterator(), false).map(node -> node.asText()).collect(Collectors.toList());
builder.project(projection);
}
private void readOrderBysJson(IQueryBuilder builder, JsonNode orderBys) {
for(JsonNode orderBy : orderBys) {
AttributeInfo info = DataStore.getInstance().getAttributeInfo(orderBy.get("info").get("name").asText());
builder.orderBy(info, orderBy.get("descending").asBoolean());
}
}
private void readPredicateJson(IQueryBuilder builder, JsonNode jsonNode) {
String type = jsonNode.get("type").asText();
switch(type) {
case "MatchPredicate":
readMatchPredicateJson(builder, jsonNode);
break;
case "AndPredicate":
readAndPredicateJson(builder, jsonNode);
break;
case "OrPredicate":
readOrPredicateJson(builder, jsonNode);
break;
case "NotPredicate":
readNotPredicateJson(builder, jsonNode);
break;
default:
throw new UnsupportedOperationException(type);
}
}
@SuppressWarnings({ "unchecked" })
private void readMatchPredicateJson(IQueryBuilder builder, JsonNode jsonNode) {
String name = jsonNode.get("info").get("name").asText();
AttributeInfo info = fetchAttributeInfo(name);
MatchOp matchOp = MatchOp.valueOf(jsonNode.get("matchOp").get("name").asText());
Object value = readJsonValue(jsonNode.get("value"), null, new HashMap<>());
if(IStore.dbIdName.equals(name) && value!=null) {
if(value instanceof Collection)
value = ((Collection
© 2015 - 2024 Weber Informatics LLC | Privacy Policy