All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.nustaq.kontraktor.services.rlserver.RLJsonSession Maven / Gradle / Ivy

There is a newer version: 5.2.0
Show newest version
package org.nustaq.kontraktor.services.rlserver;

import com.eclipsesource.json.Json;
import com.eclipsesource.json.JsonArray;
import com.eclipsesource.json.JsonObject;
import com.eclipsesource.json.JsonValue;
import org.nustaq.kontraktor.Actor;
import org.nustaq.kontraktor.Callback;
import org.nustaq.kontraktor.IPromise;
import org.nustaq.kontraktor.Promise;
import org.nustaq.kontraktor.remoting.base.RemotedActor;
import org.nustaq.kontraktor.services.rlclient.DataClient;
import org.nustaq.kontraktor.util.Log;
import org.nustaq.reallive.api.ChangeMessage;
import org.nustaq.reallive.api.RealLiveTable;
import org.nustaq.reallive.api.Record;
import org.nustaq.reallive.api.Subscriber;
import org.nustaq.reallive.impl.QueryPredicate;
import org.nustaq.reallive.impl.RLUtil;
import org.nustaq.reallive.messages.AddMessage;
import org.nustaq.reallive.messages.UpdateMessage;
import org.nustaq.reallive.query.QParseException;
import org.nustaq.reallive.records.MapRecord;

import java.lang.reflect.Array;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

public class RLJsonSession extends Actor implements RemotedActor {

    public static int senderIdRangeStart=100_000, senderIdRangeEnd = 5_000_000; // WARNING: needs organization for multiple instances

    protected static AtomicInteger senderIdCount;

    protected RLJsonServer server;
    protected DataClient dClient;
    protected int senderId;

    public void init( RLJsonServer server, DataClient dataClient, Object userdata ) {
        synchronized (RLJsonSession.class ) {
            if ( senderIdCount == null )
                senderIdCount = new AtomicInteger(senderIdRangeStart);
            senderId = senderIdCount.getAndIncrement();
            if ( senderId >= senderIdRangeEnd ) {
                senderIdCount.set(senderIdRangeStart);
            }
        }
        this.server = server;
        this.dClient = dataClient;
    }

    public IPromise getSenderId() {
        return resolve(senderId);
    }

    public IPromise update(String table, String json ) {
        try {
            updateAsync(table, json);
        } catch (Exception e) {
            String message = e.getMessage();
            return reject(message==null ? e.toString() : e );
        }
        return resolve(true);
    }

    public void updateAsync(String table, String json) {
        JsonValue parse = Json.parse(json);
        if ( ! parse.isObject() ) {
            throw new RuntimeException("not a json object:"+json);
        }
        JsonObject members = parse.asObject();
        RealLiveTable tbl = dClient.tbl(table);
        if ( tbl == null )
            throw new RuntimeException("table '"+table+"' not found");
        Record record = toRecord(members);
        if ( record.getKey() == null )
            throw new RuntimeException("no key in record");
        _internalUpdate(tbl,record);
    }

    public IPromise delete(String table, String key ) {
        try {
            deleteAsync(table, key);
        } catch (Exception e) {
            String message = e.getMessage();
            return reject(message==null ? e.toString() : e );
        }
        return resolve(true);
    }

    public IPromise> fieldsOf( String table ) {
        Promise res = new Promise();
        RealLiveTable tbl = dClient.tbl(table);
        if ( tbl == null )
            return reject("table '"+table+"' not found");
        SchemaSpore.apply(tbl).then( (r,e) -> {
            if ( r != null )
                res.resolve(r.toArray(new String[r.size()]));
            else
                res.reject(e);
        });
        return res;
    }

    public IPromise get( String table, String key ) {
        IPromise res = new Promise();
        RealLiveTable tbl = dClient.tbl(table);
        if ( tbl == null )
            reject("table '"+table+"' not found");
        tbl.get(key).then( (r,e) -> {
            if ( r != null ) {
                res.resolve(fromRecord(r).toString());
            } else
                res.reject(e);
        });
        return res;
    }

    public void select(String table, String query, Callback res) {
        RealLiveTable tbl = dClient.tbl(table);
        AtomicBoolean hadErr = new AtomicBoolean(false);
        if ( tbl == null )
            res.reject("table '"+table+"' not found");
        tbl.query(query, (r,e) -> {
            if ( r != null )
                res.pipe(fromRecord(r).toString());
            else if ( e != null ) {
                if (!hadErr.get()) {
                    if (e instanceof QParseException) {
                        res.reject("Error in Query:" + ((QParseException) e).getMessage());
                    } else
                        res.reject(e);
                    hadErr.set(true);
                } else {
                    // do nothing
                }
            }
            else
                res.finish();
        });
    }

    protected static class SubsEntry {
        Callback feCB;
        Subscriber subs;

        public SubsEntry(Callback feCB, Subscriber subs) {
            this.feCB = feCB;
            this.subs = subs;
        }
    }

    protected Map subscriptions = new HashMap<>();
    public void unsubscribe( String uuid ) {
        SubsEntry subsEntry = subscriptions.get(uuid);
        if ( subsEntry != null ) {
            Callback callback = subsEntry.feCB;
            dClient.unsubscribe(subsEntry.subs.getId());
            if (callback != null) {
                callback.finish();
                subscriptions.remove(uuid);
            }
        }
    }

    public void subscribe(String uuid, String table, String query, Callback res) {
        RealLiveTable tbl = dClient.tbl(table);
        if ( tbl == null )
            res.reject("table '"+table+"' not found");
        Subscriber subscriber = tbl.subscribeOn(query, (change) -> {
            if (change != null)
                res.pipe(fromChange(change).toString());
            else
                res.finish();
        });
        subscriptions.put(uuid, new SubsEntry(res,subscriber));
    }

    public IPromise subscribeSyncing(String uuid, String table, long timeStamp, String query, Callback res) {
        RealLiveTable tbl = dClient.tbl(table);
        if ( tbl == null )
            res.reject("table '"+table+"' not found");
        QueryPredicate filter = new QueryPredicate(query);
        Subscriber subs = new Subscriber( rec -> rec.getLastModified() >= timeStamp && filter.test(rec),(change) -> {
            if ( change != null )
                res.pipe(fromChange(change).toString());
            else
                res.finish();
        });
        subscriptions.put(uuid,new SubsEntry(res,subs));
        tbl.subscribe(subs);
        return resolve(System.currentTimeMillis());
    }

    protected JsonObject fromChange( ChangeMessage change ) {
        switch ( change.getType() ) {
            case ChangeMessage.ADD: {
                JsonObject result = Json.object();
                result.set("type", "ADD");
                result.set("senderId", change.getSenderId());
                result.set("record", fromRecord(change.getRecord()));
                return result;
            }
            case ChangeMessage.REMOVE:
            {
                JsonObject result = Json.object();
                result.set("type", "REMOVE");
                result.set("senderId", change.getSenderId());
                result.set("record", fromRecord(change.getRecord()));
                return result;
            }
            case ChangeMessage.UPDATE: {
                JsonObject result = Json.object();
                result.set("type", "UPDATE");
                result.set("senderId", change.getSenderId());
                result.set("record", fromRecord(change.getRecord()));
                JsonObject diff = new JsonObject();
                UpdateMessage upd = (UpdateMessage) change;
                String[] changedFields = upd.getDiff().getChangedFields();
                Object[] oldValues = upd.getDiff().getOldValues();
                for (int i = 0; i < changedFields.length; i++) {
                    String changedField = changedFields[i];
                    diff.set(changedField, fromJavaValue(oldValues[i]) );
                }
                result.set("diff", diff);
                return result;
            }
            case ChangeMessage.QUERYDONE:
                JsonObject result = Json.object();
                result.set("type","QUERYDONE");
                return result;
            default:
                Log.Error(this,"unexpected change type");
        }
        return null;
    }

    public void deleteAsync(String table, String key ) {
        RealLiveTable tbl = dClient.tbl(table);
        if ( tbl == null )
            throw new RuntimeException("table '"+table+"' not found");
        tbl.remove(senderId,key);
    }

    /**
     * process a series of add/update operations. Input is a map.
     * Atomic Array Ops can be done like
     * { "array+" : value } - add value to existing array
     * { "array-" : value } - remove all equal values from existing array
     * { "array?+" : value } - add only if value does not yet exist (set-like behaviour)
     * _NULL_ - can be used to denote null values (real null will be evicted by json )
     *
     * @param table
     * @param json - [ addOrUpdate, .. ]
     * @return
     */
    public IPromise bulkUpdate(String table, String json ) {
        try {
            JsonObject parse = Json.parse(json).asObject();
            RealLiveTable tbl = dClient.tbl(table);
            parse.forEach( member -> {
                member.getValue().asArray().forEach( addupd -> {
                    try {
                        JsonObject obj = addupd.asObject();
                        Record newRecord = toRecord(obj);
                        newRecord.key(member.getName());
                        _internalUpdate(tbl, newRecord);
                        // avoid getting stuck
                    } catch (Exception e) {
                        Log.Error(this,e);
                    }
                });
            });
        } catch ( Exception e ) {
            return reject(e);
        }
        return resolve(System.currentTimeMillis());
    }

    /**
     * processes special ops like { "array+" : value } and _NULL_
     * @param tbl
     * @param newRecord
     */
    protected void _internalUpdate(RealLiveTable tbl, Record newRecord) {
        int finalSID = this.senderId;
        tbl.atomic(finalSID,newRecord.getKey(), currentRecord -> {
            if ( currentRecord != null ) {
                String[] fields = newRecord.getFields();
                for (int i = 0; i < fields.length; i++) {
                    String field = fields[i];
                    if ( field.endsWith("+") ) // atomic array insert
                    {
                        boolean set = field.endsWith("?+");
                        Object toAdd = newRecord.get(field);
                        String pureField = field.substring(0,field.length()- (set ? 2 : 1));
                        Object o = currentRecord.get(pureField);
                        if ( o instanceof Object[] ) {
                            Object[] oldarr = (Object[]) o;
                            boolean matched = false;
                            if ( set ) {
                                for (int j = 0; j < oldarr.length; j++) {
                                    Object o1 = oldarr[j];
                                    if ( Objects.deepEquals(o1,toAdd) ) {
                                        matched = true;
                                        break;
                                    }
                                }
                            }
                            if ( ! matched ) {
                                Object newCopy[] = new Object[oldarr.length + 1];
                                System.arraycopy(oldarr, 0, newCopy, 0, oldarr.length);
                                newCopy[oldarr.length] = toAdd;
                                currentRecord.put(pureField, newCopy);
                            }
                        } else {
                            currentRecord.put( pureField, new Object[] { toAdd } );
                        }
                    } else if ( field.endsWith("-") ) // atomic array remove
                    {
                        String purefield = field.substring(0,field.length()-1);
                        Object toRem = newRecord.get(field);
                        if ("_NULL_".equals(toRem) )
                            toRem = null;
                        Object o = currentRecord.get(purefield);
                        Object[] oldarr = (Object[]) o;
                        Object finalToRem = toRem;
                        Object[] collect = Arrays.asList(oldarr).stream().filter(x -> !Objects.deepEquals(x, finalToRem)).collect(Collectors.toList()).toArray();
                        currentRecord.put(purefield,collect);
                    } else {
                        currentRecord.put(field,newRecord.get(field));
                    }
                }
                return null;
            }
            Object[] keyVals = newRecord.getKeyVals();
            for (int i = 0; i < keyVals.length; i+=2) {
                String keyVal = (String) keyVals[i];
                if ( keyVal.endsWith("?+") )
                    keyVals[i] = keyVal.substring(0,keyVal.length()-2);
                else if ( keyVal.endsWith("-") )
                    keyVals[i] = keyVal.substring(0,keyVal.length()-1);
                else if ( keyVal.endsWith("+") )
                    keyVals[i] = keyVal.substring(0,keyVal.length()-1);
            }
            return RLUtil.get().addOrUpdate(finalSID,newRecord.getKey(),keyVals);
        });
    }

    protected JsonObject fromRecord(Record r) {
        String[] fields = r.getFields();
        String key = r.getKey();
        JsonObject res = new JsonObject();
        if ( key != null )
            res.set("key",key);
        res.set("lastModified",r.getLastModified());
        for (int i = 0; i < fields.length; i++) {
            String field = fields[i];
            Object value = r.get(field);
            res.set(field,fromJavaValue(value));
        }
        return res;
    }

    protected JsonValue fromJavaValue(Object value) {
        if ( value instanceof String ) {
            return Json.value((String)value);
        } else if ( value instanceof Long ) {
            return Json.value(((Number) value).longValue());
        } else if ( value instanceof Integer ) {
            return Json.value(((Number) value).intValue());
        } else if ( value instanceof Number ) {
            return Json.value(((Number) value).doubleValue());
        } else if ( value instanceof Boolean ) {
            return Json.value( ((Boolean) value).booleanValue() );
        } else if ( value instanceof Object[] ) {
            JsonArray jarr = new JsonArray();
            Object arr[] = (Object[]) value;
            for (int j = 0; j < arr.length; j++) {
                jarr.add(fromJavaValue(arr[j]));
            }
            return jarr;
        } else if ( value instanceof Record ) {
            return fromRecord((Record) value);
        } else if ( value == null ) {
            return Json.NULL;
        } else {
            System.out.println("unmapped data "+value);
        }
        return Json.value(""+value);
    }

    protected Record toRecord(JsonObject members) {
        MapRecord aNew = MapRecord.New(null);
        members.names().forEach( field -> {
            if ( "key".equals(field) ) {
                aNew.key(members.get(field).asString());
                return;
            }
            JsonValue jsonValue = members.get(field);
            if ( jsonValue.isString() ) {
                aNew.put(field,jsonValue.asString());
            } else if ( jsonValue.isNull() ) {
                aNew.put(field,null);
            } else if ( jsonValue.isNumber() ) {
                if ( jsonValue.toString().indexOf('.') >= 0 )
                    aNew.put(field,jsonValue.asDouble());
                else
                    aNew.put(field,jsonValue.asLong());
            } else if ( jsonValue.isBoolean() ) {
                aNew.put(field,jsonValue.asBoolean());
            } else if ( jsonValue.isObject() ) {
                aNew.put(field,toRecord(jsonValue.asObject()));
            } else if ( jsonValue.isArray() ) {
                aNew.put(field,toRecordArray(jsonValue.asArray()));
            } else {
                throw new RuntimeException("unexpected json type:"+jsonValue.getClass());
            }
        });
        return aNew;
    }

    protected Object[] toRecordArray(JsonArray arr) {
        Object res[] = new Object[arr.size()];
        int i = 0;
        for (JsonValue jsonValue : arr) {
            if ( jsonValue.isString() ) {
                res[i] = jsonValue.asString();
            } else if ( jsonValue.isNull() ) {
                res[i] = null;
            } else if ( jsonValue.isNumber() ) {
                res[i] = jsonValue.asDouble();
            } else if ( jsonValue.isBoolean() ) {
                res[i] = jsonValue.asBoolean();
            } else if ( jsonValue.isObject() ) {
                res[i] = toRecord(jsonValue.asObject());
            } else if ( jsonValue.isArray() ) {
                res[i] = toRecordArray(jsonValue.asArray());
            } else {
                throw new RuntimeException("unexpected json type:"+jsonValue.getClass());
            }
            i++;
        }
        return res;
    }


    @Override
    public void hasBeenUnpublished(String connectionIdentifier) {

    }

    @Override
    public void hasBeenPublished(String connectionIdentifier) {

    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy