org.apache.openmeetings.web.room.wb.WbPanel.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.openmeetings.web.room.wb.WbPanel.java

Source

/*
 * 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
 *
 *   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.apache.openmeetings.web.room.wb;

import static java.util.UUID.randomUUID;
import static org.apache.openmeetings.db.dto.room.Whiteboard.ATTR_FILE_ID;
import static org.apache.openmeetings.db.dto.room.Whiteboard.ATTR_FILE_TYPE;
import static org.apache.openmeetings.db.dto.room.Whiteboard.ATTR_HEIGHT;
import static org.apache.openmeetings.db.dto.room.Whiteboard.ATTR_SLIDE;
import static org.apache.openmeetings.db.dto.room.Whiteboard.ATTR_TYPE;
import static org.apache.openmeetings.db.dto.room.Whiteboard.ATTR_WIDTH;
import static org.apache.openmeetings.db.dto.room.Whiteboard.ATTR_ZOOM;
import static org.apache.openmeetings.db.dto.room.Whiteboard.ITEMS_KEY;
import static org.apache.openmeetings.util.OpenmeetingsVariables.ATTR_CLASS;
import static org.apache.openmeetings.util.OpenmeetingsVariables.PARAM_STATUS;
import static org.apache.openmeetings.web.room.wb.WbWebSocketHelper.getObjWbJson;
import static org.apache.openmeetings.web.room.wb.WbWebSocketHelper.getWbJson;
import static org.apache.wicket.AttributeModifier.append;

import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;

import javax.imageio.ImageIO;

import org.apache.commons.codec.binary.Base64;
import org.apache.openmeetings.db.dao.file.FileItemDao;
import org.apache.openmeetings.db.dto.room.Whiteboard;
import org.apache.openmeetings.db.dto.room.Whiteboard.ZoomMode;
import org.apache.openmeetings.db.dto.room.Whiteboards;
import org.apache.openmeetings.db.entity.basic.Client;
import org.apache.openmeetings.db.entity.file.BaseFileItem;
import org.apache.openmeetings.db.entity.file.FileItem;
import org.apache.openmeetings.db.entity.room.Room;
import org.apache.openmeetings.db.entity.room.Room.Right;
import org.apache.openmeetings.db.entity.room.Room.RoomElement;
import org.apache.openmeetings.db.entity.room.RoomFile;
import org.apache.openmeetings.util.NullStringer;
import org.apache.openmeetings.util.OmFileHelper;
import org.apache.openmeetings.web.app.WhiteboardManager;
import org.apache.openmeetings.web.common.NameDialog;
import org.apache.openmeetings.web.room.RoomPanel;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.attributes.AjaxRequestAttributes;
import org.apache.wicket.ajax.attributes.AjaxRequestAttributes.Method;
import org.apache.wicket.markup.html.list.ListItem;
import org.apache.wicket.markup.html.list.ListView;
import org.apache.wicket.request.resource.JavaScriptResourceReference;
import org.apache.wicket.request.resource.ResourceReference;
import org.apache.wicket.spring.injection.annot.SpringBean;
import org.apache.wicket.util.string.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.github.openjson.JSONArray;
import com.github.openjson.JSONObject;
import com.github.openjson.JSONTokener;
import com.googlecode.wicket.jquery.ui.widget.dialog.DialogButton;

public class WbPanel extends AbstractWbPanel {
    private static final long serialVersionUID = 1L;
    private static final Logger log = LoggerFactory.getLogger(WbPanel.class);
    private static final String PARAM_UPDATED = "updated";
    private static final int UPLOAD_WB_LEFT = 0;
    private static final int UPLOAD_WB_TOP = 0;
    private static final int DEFAULT_WIDTH = 640;
    private static final int DEFAULT_HEIGHT = 480;
    private static final int UNDO_SIZE = 20;
    public static final ResourceReference WB_JS_REFERENCE = new JavaScriptResourceReference(WbPanel.class, "wb.js");
    private final Long roomId;
    private long wb2save = -1;
    private final Map<Long, Deque<UndoObject>> undoList = new HashMap<>();
    private final NameDialog fileName = new NameDialog("filename") {
        private static final long serialVersionUID = 1L;

        @Override
        protected void onSubmit(AjaxRequestTarget target, DialogButton btn) {
            String res = saveWb(roomId, wb2save, getModelObject());
            if (!Strings.isEmpty(res)) {
                error("Unexpected error while saving WB: " + res);
                target.add(feedback);
            }
        }

        @Override
        protected String getTitleStr() {
            return getString("199");
        }

        @Override
        protected String getLabelStr() {
            return getString("200");
        }

        @Override
        protected String getAddStr() {
            return getString("144");
        }
    };
    @SpringBean
    private WhiteboardManager wbm;
    @SpringBean
    private FileItemDao fileDao;

    public WbPanel(String id, RoomPanel rp) {
        super(id, rp);
        this.roomId = rp.getRoom().getId();
        if (rp.getRoom().isHidden(RoomElement.Whiteboard)) {
            setVisible(false);
        } else {
            add(new ListView<String>("clipart", Arrays.asList(OmFileHelper.getPublicClipartsDir().list()).stream()
                    .sorted().collect(Collectors.toList())) {
                private static final long serialVersionUID = 1L;

                @Override
                protected void populateItem(ListItem<String> item) {
                    String cls = String.format("clipart-%s", item.getIndex());
                    item.add(append(ATTR_CLASS, cls), append("data-mode", cls),
                            append("data-image", item.getModelObject()).setSeparator(""));
                }
            }, fileName);
        }
    }

    @Override
    void internalWbLoad(StringBuilder sb) {
        Long langId = rp.getClient().getUser().getLanguageId();
        if (!wbm.contains(roomId) && rp.getRoom().getFiles() != null && !rp.getRoom().getFiles().isEmpty()) {
            if (wbm.tryLock(roomId)) {
                try {
                    TreeMap<Long, List<BaseFileItem>> files = new TreeMap<>();
                    for (RoomFile rf : rp.getRoom().getFiles()) {
                        List<BaseFileItem> bfl = files.get(rf.getWbIdx());
                        if (bfl == null) {
                            files.put(rf.getWbIdx(), new ArrayList<>());
                            bfl = files.get(rf.getWbIdx());
                        }
                        bfl.add(rf.getFile());
                    }
                    Whiteboards _wbs = wbm.get(roomId, langId);
                    for (Map.Entry<Long, List<BaseFileItem>> e : files.entrySet()) {
                        Whiteboard wb = wbm.add(roomId, langId);
                        _wbs.setActiveWb(wb.getId());
                        for (BaseFileItem fi : e.getValue()) {
                            sendFileToWb(fi, false);
                        }
                    }
                } finally {
                    wbm.unlock(roomId);
                }
            }
        }
        Whiteboards wbs = wbm.get(roomId, langId);
        loadWhiteboards(sb, rp.getClient(), wbs, wbm.list(roomId));
        JSONObject wbj = getWbJson(wbs.getActiveWb());
        sb.append("WbArea.activateWb(").append(wbj).append(");");
        Whiteboard wb = wbs.get(wbs.getActiveWb());
        if (wb != null) {
            sb.append("WbArea.setSlide(").append(wbj.put(ATTR_SLIDE, wb.getSlide())).append(");");
        }
        sb.append("WbArea.loadVideos();");
    }

    @Override
    protected void updateWbActionAttributes(AjaxRequestAttributes attributes) {
        attributes.setMethod(Method.POST);
    }

    @Override
    protected void processWbAction(WbAction a, JSONObject obj, AjaxRequestTarget target) throws IOException {
        Client c = rp.getClient();
        if (c == null) {
            return;
        }
        switch (a) {
        case createObj:
        case modifyObj: {
            JSONObject o = obj.optJSONObject("obj");
            if (o != null && "pointer".equals(o.getString(ATTR_TYPE))) {
                sendWbOthers(a, obj);
                return;
            }
        }
            break;
        case downloadPdf: {
            boolean moder = c.hasRight(Room.Right.moderator);
            Room r = rp.getRoom();
            if ((moder && !r.isHidden(RoomElement.ActionMenu)) || (!moder && r.isAllowUserQuestions())) {
                try (PDDocument doc = new PDDocument()) {
                    JSONArray arr = obj.getJSONArray("slides");
                    for (int i = 0; i < arr.length(); ++i) {
                        String base64Image = arr.getString(i).split(",")[1];
                        byte[] bb = Base64.decodeBase64(base64Image);
                        BufferedImage img = ImageIO.read(new ByteArrayInputStream(bb));
                        float width = img.getWidth();
                        float height = img.getHeight();
                        PDPage page = new PDPage(new PDRectangle(width, height));
                        PDImageXObject pdImageXObject = LosslessFactory.createFromImage(doc, img);
                        try (PDPageContentStream contentStream = new PDPageContentStream(doc, page,
                                AppendMode.APPEND, false)) {
                            contentStream.drawImage(pdImageXObject, 0, 0, width, height);
                        }
                        doc.addPage(page);
                    }
                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    doc.save(baos);
                    rp.startDownload(target, baos.toByteArray());
                }
            }
            return;
        }
        case loadVideos: {
            StringBuilder sb = new StringBuilder("WbArea.initVideos(");
            JSONArray arr = new JSONArray();
            for (Entry<Long, Whiteboard> entry : wbm.list(roomId)) {
                Whiteboard wb = entry.getValue();
                for (JSONObject o : wb.list()) {
                    String ft = o.optString(ATTR_FILE_TYPE);
                    if (BaseFileItem.Type.Recording.name().equals(ft)
                            || BaseFileItem.Type.Video.name().equals(ft)) {
                        JSONObject _sts = o.optJSONObject(PARAM_STATUS);
                        if (_sts == null) {
                            continue;
                        }
                        JSONObject sts = new JSONObject(_sts.toString()); //copy
                        sts.put("pos", sts.getDouble("pos")
                                + (System.currentTimeMillis() - sts.getLong(PARAM_UPDATED)) * 1. / 1000);
                        arr.put(new JSONObject().put("wbId", wb.getId()).put("uid", o.getString("uid"))
                                .put(ATTR_SLIDE, o.getString(ATTR_SLIDE)).put(PARAM_STATUS, sts));
                    }
                }
            }
            sb.append(arr.toString()).append(");");
            target.appendJavaScript(sb);
            return;
        }
        default:
            break;
        }

        //presenter-right
        if (c.hasRight(Right.presenter)) {
            switch (a) {
            case createWb: {
                Whiteboard wb = wbm.add(roomId, c.getUser().getLanguageId());
                sendWbAll(WbAction.createWb, getAddWbJson(wb));
            }
                break;
            case removeWb: {
                long id = obj.optLong("wbId", -1);
                if (id > -1) {
                    wbm.remove(roomId, id);
                    sendWbAll(WbAction.removeWb, obj);
                }
            }
                break;
            case activateWb: {
                long _id = obj.optLong("wbId", -1);
                if (_id > -1) {
                    wbm.activate(roomId, _id);
                    sendWbAll(WbAction.activateWb, obj);
                }
            }
                break;
            case renameWb: {
                Whiteboard wb = wbm.get(roomId).get(obj.optLong("wbId", -1));
                if (wb != null) {
                    wbm.update(roomId, wb.setName(obj.getString("name")));
                    sendWbAll(WbAction.renameWb, obj);
                }
            }
                break;
            case setSlide: {
                Whiteboard wb = wbm.get(roomId).get(obj.optLong("wbId", -1));
                if (wb != null) {
                    wb.setSlide(obj.optInt(ATTR_SLIDE, 0));
                    wbm.update(roomId, wb);
                    sendWbOthers(WbAction.setSlide, obj);
                }
            }
                break;
            case clearAll: {
                clearAll(roomId, obj.getLong("wbId"));
            }
                break;
            case setSize: {
                Whiteboard wb = wbm.get(roomId).get(obj.getLong("wbId"));
                wb.setWidth(obj.getInt(ATTR_WIDTH));
                wb.setHeight(obj.getInt(ATTR_HEIGHT));
                wb.setZoom(obj.getDouble(ATTR_ZOOM));
                wb.setZoomMode(ZoomMode.valueOf(obj.getString("zoomMode")));
                wbm.update(roomId, wb);
                sendWbOthers(WbAction.setSize, getAddWbJson(wb));
            }
                break;
            default:
                break;
            }
        }
        //wb-right
        if (c.hasRight(Right.presenter) || c.hasRight(Right.whiteBoard)) {
            switch (a) {
            case createObj: {
                Whiteboard wb = wbm.get(roomId).get(obj.getLong("wbId"));
                JSONObject o = obj.getJSONObject("obj");
                wb.put(o.getString("uid"), o);
                wbm.update(roomId, wb);
                addUndo(wb.getId(), new UndoObject(UndoObject.Type.add, o));
                sendWbOthers(WbAction.createObj, obj);
            }
                break;
            case modifyObj: {
                Whiteboard wb = wbm.get(roomId).get(obj.getLong("wbId"));
                JSONArray arr = obj.getJSONArray("obj");
                JSONArray undo = new JSONArray();
                for (int i = 0; i < arr.length(); ++i) {
                    JSONObject _o = arr.getJSONObject(i);
                    String uid = _o.getString("uid");
                    JSONObject po = wb.get(uid);
                    if (po != null) {
                        undo.put(po);
                        wb.put(uid, _o);
                    }
                }
                if (arr.length() != 0) {
                    wbm.update(roomId, wb);
                    addUndo(wb.getId(), new UndoObject(UndoObject.Type.modify, undo));
                }
                sendWbOthers(WbAction.modifyObj, obj);
            }
                break;
            case deleteObj: {
                Whiteboard wb = wbm.get(roomId).get(obj.getLong("wbId"));
                JSONArray arr = obj.getJSONArray("obj");
                JSONArray undo = new JSONArray();
                for (int i = 0; i < arr.length(); ++i) {
                    JSONObject _o = arr.getJSONObject(i);
                    JSONObject u = wb.remove(_o.getString("uid"));
                    if (u != null) {
                        undo.put(u);
                    }
                }
                if (undo.length() != 0) {
                    wbm.update(roomId, wb);
                    addUndo(wb.getId(), new UndoObject(UndoObject.Type.remove, undo));
                }
                sendWbAll(WbAction.deleteObj, obj);
            }
                break;
            case clearSlide: {
                Whiteboard wb = wbm.get(roomId).get(obj.getLong("wbId"));
                JSONArray arr = wb.clearSlide(obj.getInt(ATTR_SLIDE));
                if (arr.length() != 0) {
                    wbm.update(roomId, wb);
                    addUndo(wb.getId(), new UndoObject(UndoObject.Type.remove, arr));
                }
                sendWbAll(WbAction.clearSlide, obj);
            }
                break;
            case save:
                wb2save = obj.getLong("wbId");
                fileName.open(target);
                break;
            case undo: {
                Long wbId = obj.getLong("wbId");
                UndoObject uo = getUndo(wbId);
                if (uo != null) {
                    Whiteboard wb = wbm.get(roomId).get(wbId);
                    switch (uo.getType()) {
                    case add: {
                        JSONObject o = new JSONObject(uo.getObject());
                        wb.remove(o.getString("uid"));
                        wbm.update(roomId, wb);
                        sendWbAll(WbAction.deleteObj, obj.put("obj", new JSONArray().put(o)));
                    }
                        break;
                    case remove: {
                        JSONArray arr = new JSONArray(uo.getObject());
                        for (int i = 0; i < arr.length(); ++i) {
                            JSONObject o = arr.getJSONObject(i);
                            wb.put(o.getString("uid"), o);
                        }
                        wbm.update(roomId, wb);
                        sendWbAll(WbAction.createObj, obj.put("obj", new JSONArray(uo.getObject())));
                    }
                        break;
                    case modify: {
                        JSONArray arr = new JSONArray(uo.getObject());
                        for (int i = 0; i < arr.length(); ++i) {
                            JSONObject o = arr.getJSONObject(i);
                            wb.put(o.getString("uid"), o);
                        }
                        wbm.update(roomId, wb);
                        sendWbAll(WbAction.modifyObj, obj.put("obj", arr));
                    }
                        break;
                    }
                }
            }
                break;
            case videoStatus: {
                Whiteboard wb = wbm.get(roomId).get(obj.getLong("wbId"));
                String uid = obj.getString("uid");
                JSONObject po = wb.get(uid);
                if (po != null && "video".equals(po.getString(ATTR_TYPE))) {
                    JSONObject ns = obj.getJSONObject(PARAM_STATUS);
                    po.put(PARAM_STATUS, ns.put(PARAM_UPDATED, System.currentTimeMillis()));
                    wbm.update(roomId, wb.put(uid, po));
                    obj.put(ATTR_SLIDE, po.getInt(ATTR_SLIDE));
                    sendWbAll(WbAction.videoStatus, obj);
                }
            }
                break;
            default:
                break;
            }
        }
    }

    private static JSONObject getAddWbJson(final Whiteboard wb) {
        return new JSONObject().put("wbId", wb.getId()).put("name", wb.getName()).put(ATTR_WIDTH, wb.getWidth())
                .put(ATTR_HEIGHT, wb.getHeight()).put(ATTR_ZOOM, wb.getZoom())
                .put("zoomMode", wb.getZoomMode().name());
    }

    @Override
    protected String getRole() {
        String role = ROLE_NONE;
        if (rp.getClient().hasRight(Right.presenter)) {
            role = Right.presenter.name();
        } else if (rp.getClient().hasRight(Right.whiteBoard)) {
            role = Right.whiteBoard.name();
        }
        return role;
    }

    private JSONObject addFileUrl(Client cl, String ruid, JSONObject _file) {
        return addFileUrl(cl, ruid, _file, null);
    }

    private JSONObject addFileUrl(Client cl, String ruid, JSONObject _file, Consumer<BaseFileItem> consumer) {
        try {
            final long fid = _file.optLong(ATTR_FILE_ID, -1);
            if (fid > 0) {
                BaseFileItem fi = fileDao.getAny(fid);
                if (fi != null) {
                    if (consumer != null) {
                        consumer.accept(fi);
                    }
                    return WbWebSocketHelper.addFileUrl(ruid, _file, fi, cl);
                }
            }
        } catch (Exception e) {
            //no-op, non-file object
        }
        return _file;
    }

    private static JSONArray getArray(JSONObject wb, Function<JSONObject, JSONObject> postprocess) {
        JSONObject items = wb.getJSONObject(ITEMS_KEY);
        JSONArray arr = new JSONArray();
        for (String uid : items.keySet()) {
            JSONObject o = items.getJSONObject(uid);
            if (postprocess != null) {
                o = postprocess.apply(o);
            }
            arr.put(o);
        }
        return arr;
    }

    private void clearAll(Long roomId, long wbId) {
        Whiteboard wb = wbm.get(roomId).get(wbId);
        if (wb == null) {
            return;
        }
        JSONArray arr = getArray(wb.toJson(), null);
        if (arr.length() != 0) {
            addUndo(wb.getId(), new UndoObject(UndoObject.Type.remove, arr));
        }
        wb = wbm.clear(roomId, wbId);
        sendWbAll(WbAction.clearAll, new JSONObject().put("wbId", wbId));
        sendWbAll(WbAction.setSize, getAddWbJson(wb));
    }

    private static void updateWbSize(Whiteboard wb, final BaseFileItem fi) {
        int w = fi.getWidth() == null ? DEFAULT_WIDTH : fi.getWidth();
        int h = fi.getHeight() == null ? DEFAULT_HEIGHT : fi.getHeight();
        double scale = 1. * wb.getWidth() / w;
        scale = scale < 1 ? 1 : scale;
        wb.setWidth(Math.max(wb.getWidth(), (int) (w * scale)));
        wb.setHeight(Math.max(wb.getHeight(), (int) (h * scale)));
    }

    @Override
    public void sendFileToWb(final BaseFileItem fi, boolean clean) {
        if (isVisible() && fi.getId() != null) {
            Whiteboards wbs = wbm.get(roomId);
            String wuid = randomUUID().toString();
            Whiteboard wb = wbs.get(wbs.getActiveWb());
            if (wb == null) {
                return;
            }
            switch (fi.getType()) {
            case Folder:
                //do nothing
                break;
            case WmlFile: {
                File f = fi.getFile();
                if (f.exists() && f.isFile()) {
                    try (BufferedReader br = Files.newBufferedReader(f.toPath())) {
                        final boolean[] updated = { false };
                        JSONArray arr = getArray(new JSONObject(new JSONTokener(br)), o -> {
                            wb.put(o.getString("uid"), o);
                            updated[0] = true;
                            return addFileUrl(rp.getClient(), wbs.getUid(), o, _f -> updateWbSize(wb, _f));
                        });
                        if (updated[0]) {
                            wbm.update(roomId, wb);
                        }
                        sendWbAll(WbAction.setSize, getAddWbJson(wb));
                        sendWbAll(WbAction.load, getObjWbJson(wb.getId(), arr));
                    } catch (Exception e) {
                        log.error("Unexpected error while loading WB", e);
                    }
                }
            }
                break;
            case PollChart:
                break;
            default: {
                JSONObject file = new JSONObject().put(ATTR_FILE_ID, fi.getId())
                        .put(ATTR_FILE_TYPE, fi.getType().name()).put("count", fi.getCount())
                        .put(ATTR_TYPE, "image").put("left", UPLOAD_WB_LEFT).put("top", UPLOAD_WB_TOP)
                        .put(ATTR_WIDTH, fi.getWidth() == null ? DEFAULT_WIDTH : fi.getWidth())
                        .put(ATTR_HEIGHT, fi.getHeight() == null ? DEFAULT_HEIGHT : fi.getHeight()).put("uid", wuid)
                        .put(ATTR_SLIDE, wb.getSlide());
                if (FileItem.Type.Video == fi.getType() || FileItem.Type.Recording == fi.getType()) {
                    file.put(ATTR_TYPE, "video");
                    file.put(PARAM_STATUS, new JSONObject().put("paused", true).put("pos", 0.0).put(PARAM_UPDATED,
                            System.currentTimeMillis()));
                }
                final String ruid = wbs.getUid();
                if (clean) {
                    clearAll(roomId, wb.getId());
                }
                wb.put(wuid, file);
                updateWbSize(wb, fi);
                wbm.update(roomId, wb);
                sendWbAll(WbAction.setSize, getAddWbJson(wb));
                WbWebSocketHelper.sendWbFile(roomId, wb.getId(), ruid, file, fi);
            }
                break;
            }
        }
    }

    private void sendWbOthers(WbAction a, JSONObject obj) {
        WbWebSocketHelper.sendWbOthers(roomId, a, obj, rp.getClient().getUid());
    }

    private void sendWbAll(WbAction a, JSONObject obj) {
        WbWebSocketHelper.sendWbAll(roomId, a, obj);
    }

    private void addUndo(Long wbId, UndoObject u) {
        if (wbId == null) {
            return;
        }
        if (!undoList.containsKey(wbId)) {
            undoList.put(wbId, new LimitedLinkedList<>());
        }
        undoList.get(wbId).push(u);
    }

    private UndoObject getUndo(Long wbId) {
        if (wbId == null || !undoList.containsKey(wbId)) {
            return null;
        }
        Deque<UndoObject> deq = undoList.get(wbId);
        return deq.isEmpty() ? null : deq.pop();
    }

    private static class LimitedLinkedList<T> extends LinkedList<T> {
        private static final long serialVersionUID = 1L;

        @Override
        public void push(T e) {
            super.push(e);
            while (size() > UNDO_SIZE) {
                removeLast();
            }
        }
    }

    public String saveWb(Long roomId, Long wbId, String name) {
        Whiteboard wb = wbm.get(roomId).get(wbId);
        FileItem f = new FileItem();
        f.setType(BaseFileItem.Type.WmlFile);
        f.setRoomId(roomId);
        f.setHash(randomUUID().toString());
        f.setName(name);
        f = fileDao.update(f);
        return wb.save(f.getFile().toPath());
    }

    private StringBuilder loadWhiteboards(StringBuilder sb, Client cl, Whiteboards wbs,
            Set<Entry<Long, Whiteboard>> boardSet) {
        for (Entry<Long, Whiteboard> entry : boardSet) {
            Whiteboard wb = entry.getValue();
            sb.append(new StringBuilder("WbArea.create(").append(getAddWbJson(wb)).append(");"));
            JSONArray arr = new JSONArray();
            for (JSONObject o : wb.list()) {
                arr.put(addFileUrl(cl, wbs.getUid(), o));
            }
            sb.append("WbArea.load(").append(getObjWbJson(entry.getKey(), arr).toString(new NullStringer()))
                    .append(");");
        }
        return sb;
    }
}