org.kontalk.model.KonMessage.java Source code

Java tutorial

Introduction

Here is the source code for org.kontalk.model.KonMessage.java

Source

/*
 *  Kontalk Java client
 *  Copyright (C) 2014 Kontalk Devteam <devteam@kontalk.org>
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.kontalk.model;

import java.util.Date;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Observable;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
import org.kontalk.system.Database;
import org.kontalk.crypto.Coder;
import org.kontalk.util.EncodingUtils;

/**
 * Base class for incoming and outgoing XMMP messages.
 * @author Alexander Bikadorov {@literal <bikaejkb@mail.tu-berlin.de>}
 */
public class KonMessage extends Observable implements Comparable<KonMessage> {
    private static final Logger LOGGER = Logger.getLogger(KonMessage.class.getName());

    /**
     * Direction (in-, outgoing) of one message.
     * Do not modify, only add! Ordinal used in database.
     */
    public static enum Direction {
        IN, OUT
    };

    /**
     * Sending status of one message.
     * Do not modify, only add! Ordinal used in database
     */
    public static enum Status {
        /** For all incoming messages. */
        IN,
        //ACKNOWLEDGED,
        /** Outgoing message, message is about to be send. */
        PENDING,
        /** Outgoing message, message was handled by server. */
        SENT,
        /** Outgoing message, message was received by recipient. */
        RECEIVED,
        /** Outgoing message, an error occurred somewhere in the transmission. */
        ERROR
    };

    public static final String TABLE = "messages";
    public static final String COL_THREAD_ID = "thread_id";
    public static final String COL_DIR = "direction";
    public static final String COL_USER_ID = "user_id";
    public static final String COL_JID = "jid";
    public static final String COL_XMPP_ID = "xmpp_id";
    public static final String COL_DATE = "date";
    public static final String COL_REC_STAT = "receipt_status";
    public static final String COL_CONTENT = "content";
    public static final String COL_ENCR_STAT = "encryption_status";
    public static final String COL_SIGN_STAT = "signing_status";
    public static final String COL_COD_ERR = "coder_errors";
    public static final String COL_SERV_ERR = "server_error";
    public static final String COL_SERV_DATE = "server_date";
    public static final String CREATE_TABLE = "( " + "_id INTEGER PRIMARY KEY AUTOINCREMENT, " + COL_THREAD_ID
            + " INTEGER NOT NULL, " +
            // enum, in- or outgoing
            COL_DIR + " INTEGER NOT NULL, " +
            // from or to user
            COL_USER_ID + " INTEGER NOT NULL, " +
            // full jid with resource
            COL_JID + " TEXT NOT NULL, " +
            // XMPP ID attribute; only recommended (RFC 6120), but we generate
            // a random string if not in message for model consistency
            // Note: must be unique only within a stream (RFC 6120)
            COL_XMPP_ID + " TEXT NOT NULL, " +
            // unix time, local creation timestamp
            COL_DATE + " INTEGER NOT NULL, " +
            // enum, server receipt status
            COL_REC_STAT + " INTEGER NOT NULL, " +
            // message content in JSON format
            COL_CONTENT + " TEXT NOT NULL, " +
            // enum, determines if content is encrypted
            COL_ENCR_STAT + " INTEGER NOT NULL, " +
            // enum, determines if content is verified
            // can only tell if signed after encryption attempt
            COL_SIGN_STAT + " INTEGER NOT NULL, " +
            // enum set, encryption and signing errors of content
            COL_COD_ERR + " INTEGER NOT NULL, " +
            // optional error reply in JSON format
            COL_SERV_ERR + " TEXT, " +
            // unix time, transmission/delay timestamp
            COL_SERV_DATE + " INTEGER, " +
            // if this combinations is equal we consider messages to be equal
            // (see .equals())
            "UNIQUE (direction, jid, xmpp_id, date), " + "FOREIGN KEY (thread_id) REFERENCES " + KonThread.TABLE
            + " (_id), " + "FOREIGN KEY (user_id) REFERENCES " + User.TABLE + " (_id) " + ")";

    private int mID;
    private final KonThread mThread;
    private final Direction mDir;
    private final User mUser;

    private final String mJID;
    private final String mXMPPID;

    // (local) creation time
    private final Date mDate;
    // last timestamp of server transmission packet
    // incoming: (delayed) sent; outgoing: sent/received/error
    protected Optional<Date> mServerDate;
    protected Status mReceiptStatus;
    protected final MessageContent mContent;

    protected CoderStatus mCoderStatus;

    protected ServerError mServerError;

    protected KonMessage(Builder builder) {
        mID = builder.mID;
        mThread = builder.mThread;
        mDir = builder.mDir;
        // TODO group message stuff
        mUser = builder.mUser;
        mJID = builder.mJID;
        mXMPPID = builder.mXMPPID;
        mDate = builder.mDate;
        mServerDate = builder.mServerDate;
        mReceiptStatus = builder.mReceiptStatus;
        mContent = builder.mContent;
        mCoderStatus = builder.mCoderStatus;
        mServerError = builder.mServerError;

        if (mJID == null || mXMPPID == null || mDate == null || mServerDate == null || mReceiptStatus == null
                || mContent == null || mCoderStatus == null || mServerError == null)
            throw new IllegalStateException();

        if (mID < 0)
            this.save();
    }

    /**
     * Get the database ID. <br>
     * -1 : message is not saved in database <br>
     * {@literal <} -1 : unexpected error on insertion attempt
     * @return ID of message in db
     */
    public int getID() {
        return mID;
    }

    public KonThread getThread() {
        return mThread;
    }

    public Direction getDir() {
        return mDir;
    }

    public User getUser() {
        return mUser;
    }

    public String getJID() {
        return mJID;
    }

    public String getXMPPID() {
        return mXMPPID;
    }

    public Date getDate() {
        return mDate;
    }

    public Optional<Date> getServerDate() {
        return mServerDate;
    }

    public Status getReceiptStatus() {
        return mReceiptStatus;
    }

    public MessageContent getContent() {
        return mContent;
    }

    public CoderStatus getCoderStatus() {
        return mCoderStatus;
    }

    public void setSecurityErrors(EnumSet<Coder.Error> errors) {
        mCoderStatus.setSecurityErrors(errors);
        this.save();
    }

    public ServerError getServerError() {
        return mServerError;
    }

    /**
     * Save (or insert) this message to/into the database.
     */
    public final void save() {
        if (mID < 0) {
            this.insert();
            return;
        }
        Database db = Database.getInstance();
        Map<String, Object> set = new HashMap<>();
        set.put(COL_REC_STAT, mReceiptStatus);
        set.put(COL_CONTENT, mContent.toJSONString());
        set.put(COL_ENCR_STAT, mCoderStatus.getEncryption());
        set.put(COL_SIGN_STAT, mCoderStatus.getSigning());
        set.put(COL_COD_ERR, mCoderStatus.getErrors());
        set.put(COL_SERV_ERR, mServerError.toJSON());
        set.put(COL_SERV_DATE, mServerDate);
        db.execUpdate(TABLE, set, mID);
    }

    private void insert() {
        if (mID >= 0) {
            LOGGER.warning("message already in db, ID: " + mID);
            return;
        }
        Database db = Database.getInstance();

        List<Object> values = new LinkedList<>();
        values.add(mThread.getID());
        values.add(mDir);
        values.add(mUser.getID());
        values.add(mJID);
        values.add(Database.setString(mXMPPID));
        values.add(mDate);
        values.add(mReceiptStatus);
        // i simply don't like to save all possible content explicitly in the
        // database, so we use JSON here
        values.add(mContent.toJSONString());
        values.add(mCoderStatus.getEncryption());
        values.add(mCoderStatus.getSigning());
        values.add(mCoderStatus.getErrors());
        values.add(mServerError.toJSON());
        values.add(mServerDate);

        int id = db.execInsert(TABLE, values);
        if (id <= 0) {
            LOGGER.log(Level.WARNING, "db, could not insert message");
            mID = -2;
            return;
        }
        mID = id;
    }

    boolean delete() {
        Database db = Database.getInstance();
        return db.execDelete(TABLE, mID);
    }

    protected synchronized void changed(Object arg) {
        this.setChanged();
        this.notifyObservers(arg);
    }

    @Override
    public String toString() {
        return "M:id=" + mID + ",thread=" + mThread + ",dir=" + mDir + ",mUser=" + mUser + ",jid=" + mJID
                + ",xmppid=" + mXMPPID + ",date=" + mDate + ",sdate=" + mServerDate + ",recstat=" + mReceiptStatus
                + ",cont=" + mContent + ",codstat=" + mCoderStatus + ",serverr=" + mServerError;
    }

    /**
     * Return if two messages are logically equal.
     * Inconsistent with "natural ordering"!
     */
    @Override
    public boolean equals(Object obj) {
        // performance optimization
        if (this == obj)
            return true;

        if (!(obj instanceof KonMessage))
            return false;

        KonMessage o = (KonMessage) obj;
        // note: use ONLY final fields
        if (mID == o.mID) {
            LOGGER.warning("different messages have same ID: " + mID);
            return true;
        }
        return mDir == o.mDir && mJID.equals(o.mJID) && mXMPPID.equals(o.mXMPPID) && mDate.equals(o.mDate);
    }

    @Override
    public int hashCode() {
        int hash = 7;
        hash = 67 * hash + Objects.hashCode(this.mDir);
        hash = 67 * hash + Objects.hashCode(this.mJID);
        hash = 67 * hash + Objects.hashCode(this.mXMPPID);
        hash = 67 * hash + Objects.hashCode(this.mDate);
        return hash;
    }

    @Override
    public int compareTo(KonMessage o) {
        if (this.equals(o))
            return 0;

        return Integer.compare(mID, o.getID());
    }

    public static final class ServerError {
        private static final String JSON_COND = "cond";
        private static final String JSON_TEXT = "text";

        public final String condition;
        public final String text;

        private ServerError() {
            this("", "");
        }

        ServerError(String condition, String text) {
            this.condition = condition;
            this.text = text;
        }

        private String toJSON() {
            JSONObject json = new JSONObject();
            EncodingUtils.putJSON(json, JSON_COND, condition);
            EncodingUtils.putJSON(json, JSON_TEXT, text);
            return json.toJSONString();
        }

        @Override
        public String toString() {
            return this.toJSON();
        }

        static ServerError fromJSON(String jsonContent) {
            Object obj = JSONValue.parse(jsonContent);
            Map<?, ?> map = (Map) obj;
            if (map == null)
                return new ServerError();
            String condition = EncodingUtils.getJSON(map, JSON_COND);
            String text = EncodingUtils.getJSON(map, JSON_TEXT);
            return new ServerError(condition, text);
        }
    }

    static class Builder {
        private final int mID;
        private final KonThread mThread;
        private final Direction mDir;
        private final User mUser;
        private final Date mDate;

        private ServerError mServerError = new ServerError();

        protected String mJID = null;
        protected String mXMPPID = null;

        protected Optional<Date> mServerDate = null;
        protected Status mReceiptStatus = null;
        protected MessageContent mContent = null;

        protected CoderStatus mCoderStatus = null;

        // used by subclasses and when loading from database
        Builder(int id, KonThread thread, Direction dir, User user, Date date) {
            mID = id;
            mThread = thread;
            mDir = dir;
            mUser = user;
            mDate = date;
        }

        public void jid(String jid) {
            mJID = jid;
        }

        public void xmppID(String xmppID) {
            mXMPPID = xmppID;
        }

        public void serverDate(Optional<Date> date) {
            mServerDate = date;
        }

        public void content(MessageContent content) {
            mContent = content;
        }

        void receiptStatus(Status status) {
            mReceiptStatus = status;
        }

        void coderStatus(CoderStatus coderStatus) {
            mCoderStatus = coderStatus;
        }

        void serverError(ServerError error) {
            mServerError = error;
        }

        KonMessage build() {
            if (mDir == Direction.IN)
                return new InMessage(this);
            else
                return new OutMessage(this);
        }
    }
}