blob: 1b830283bd79d99bb3391d902e54564e66e94d4b [file] [log] [blame]
/*
* 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_OMTYPE;
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.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
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.function.Consumer;
import java.util.function.Function;
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.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.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.core.request.handler.IPartialPageRequestHandler;
import org.apache.wicket.markup.html.list.ListItem;
import org.apache.wicket.markup.html.list.ListView;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.ResourceModel;
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.danekja.java.util.function.serializable.SerializableConsumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.github.openjson.JSONArray;
import com.github.openjson.JSONObject;
import com.github.openjson.JSONTokener;
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) {
String res = saveWb(roomId, wb2save, getModelObject());
if (!Strings.isEmpty(res)) {
error("Unexpected error while saving WB: " + res);
target.add(feedback);
} else {
super.onSubmit(target);
}
}
@Override
protected IModel<String> getTitle() {
return new ResourceModel("199");
}
@Override
protected IModel<String> getLabel() {
return new ResourceModel("200");
}
@Override
protected IModel<String> getAddBtnLabel() {
return new ResourceModel("144");
}
};
private final SerializableConsumer<Whiteboard> addUndo = wb -> {
JSONArray arr = getArray(wb.toJson(), null);
if (arr.length() != 0) {
addUndo(wb.getId(), new UndoObject(UndoObject.Type.REMOVE, arr));
}
};
@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<>("clipart"
, List.of(OmFileHelper.getPublicClipartsDir().list())
.stream()
.sorted()
.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();
wbm.initFiles(rp.getRoom(), langId, (wbs, wbIdx, roomFiles) -> {
roomFiles.forEach(rf -> addFileToWb(wbs, wbIdx, rf.getFile(), false, false));
});
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
public void reloadWb(IPartialPageRequestHandler handler) {
StringBuilder sb = new StringBuilder("WbArea.doCleanAll();");
internalWbLoad(sb);
handler.appendJavaScript(sb);
}
@Override
public void processWbAction(WbAction a, JSONObject obj, IPartialPageRequestHandler handler) throws IOException {
Client c = rp.getClient();
if (c == null) {
return;
}
switch (a) {
case CREATE_OBJ, MODIFY_OBJ:
{
JSONObject o = obj.optJSONObject("obj");
if (o != null && "pointer".equals(o.getString(ATTR_OMTYPE))) {
sendWbOthers(a, obj);
return;
}
}
break;
case DOWNLOAD:
{
boolean moder = c.hasRight(Room.Right.MODERATOR);
Room r = rp.getRoom();
if (moder && !r.isHidden(RoomElement.ACTION_MENU)) {
rp.startDownload(handler, obj.getString("type"), obj.getString("fuid"));
}
return;
}
case LOAD_VIDEOS:
{
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 status = o.optJSONObject(PARAM_STATUS);
if (status == null) {
continue;
}
JSONObject sts = new JSONObject(status.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(");");
handler.appendJavaScript(sb);
return;
}
default:
break;
}
//presenter-right
if (c.hasRight(Right.PRESENTER)) {
switch (a) {
case CREATE_WB:
{
Whiteboard wb = wbm.add(roomId, c.getUser().getLanguageId());
sendWbAll(WbAction.CREATE_WB, wb.getAddJson());
}
break;
case REMOVE_WB:
{
long id = obj.optLong("wbId", -1);
if (id > -1) {
long prevId = obj.optLong("prevWbId", -1);
wbm.remove(roomId, id, prevId);
sendWbAll(WbAction.REMOVE_WB, obj);
}
}
break;
case ACTIVATE_WB:
{
long wbId = obj.optLong("wbId", -1);
if (wbId > -1) {
wbm.activate(roomId, wbId);
sendWbAll(WbAction.ACTIVATE_WB, obj);
}
}
break;
case RENAME_WB:
{
Whiteboard wb = wbm.get(roomId).get(obj.optLong("wbId", -1));
if (wb != null) {
wbm.update(roomId, wb.setName(obj.getString("name")));
sendWbAll(WbAction.RENAME_WB, obj);
}
}
break;
case SET_SLIDE:
{
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.SET_SLIDE, obj);
}
}
break;
case CLEAR_ALL:
{
wbm.clearAll(roomId, obj.getLong("wbId"), addUndo);
}
break;
case SET_SIZE:
{
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.SET_SIZE, wb.getAddJson());
}
break;
default:
break;
}
}
//wb-right
if (c.hasRight(Right.PRESENTER) || c.hasRight(Right.WHITEBOARD)) {
switch (a) {
case CREATE_OBJ:
{
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.CREATE_OBJ, obj);
}
break;
case MODIFY_OBJ:
{
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 oi = arr.getJSONObject(i);
String uid = oi.getString("uid");
JSONObject po = wb.get(uid);
if (po != null) {
undo.put(po);
wb.put(uid, oi);
}
}
if (arr.length() != 0) {
wbm.update(roomId, wb);
addUndo(wb.getId(), new UndoObject(UndoObject.Type.MODIFY, undo));
}
sendWbOthers(WbAction.MODIFY_OBJ, obj);
}
break;
case DELETE_OBJ:
{
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 oi = arr.getJSONObject(i);
JSONObject u = wb.remove(oi.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.DELETE_OBJ, obj);
}
break;
case CLEAR_SLIDE:
{
wbm.cleanSlide(roomId, obj.getLong("wbId"), obj.getInt(ATTR_SLIDE)
, (wb, arr) -> addUndo(wb.getId(), new UndoObject(UndoObject.Type.REMOVE, arr)));
}
break;
case SAVE:
wb2save = obj.getLong("wbId");
fileName.show(handler);
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.DELETE_OBJ, 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.CREATE_OBJ, 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.MODIFY_OBJ, obj.put("obj", arr));
}
break;
}
}
}
break;
case VIDEO_STATUS:
{
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_OMTYPE))) {
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.VIDEO_STATUS, obj);
}
}
break;
default:
break;
}
}
}
@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 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)));
}
private void addFileToWb(Whiteboards wbs, Whiteboard wb, final BaseFileItem fi, boolean clean, boolean sendAndUpdate) {
if (fi.getId() == null || wb == null) {
return;
}
switch (fi.getType()) {
case FOLDER:
//do nothing
break;
case WML_FILE:
{
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, bf -> updateWbSize(wb, bf));
});
if (sendAndUpdate) {
if (updated[0]) {
wbm.update(roomId, wb);
}
sendWbAll(WbAction.SET_SIZE, wb.getAddJson());
sendWbAll(WbAction.LOAD, getObjWbJson(wb.getId(), arr));
}
} catch (Exception e) {
log.error("Unexpected error while loading WB", e);
}
}
}
break;
case POLL_CHART:
break;
default:
{
String wuid = randomUUID().toString();
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 (BaseFileItem.Type.VIDEO == fi.getType() || BaseFileItem.Type.RECORDING == fi.getType()) {
file.put(ATTR_OMTYPE, "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) {
wbm.clearAll(roomId, wb.getId(), addUndo);
}
wb.put(wuid, file);
updateWbSize(wb, fi);
if (sendAndUpdate) {
wbm.update(roomId, wb);
sendWbAll(WbAction.SET_SIZE, wb.getAddJson());
WbWebSocketHelper.sendWbFile(roomId, wb.getId(), ruid, file, fi);
}
}
break;
}
}
private void sendFileToWb(Long wbId, final BaseFileItem fi, boolean clean) {
if (isVisible()) {
Whiteboards wbs = wbm.get(roomId);
Whiteboard wb = wbs.get(wbId == null ? wbs.getActiveWb() : wbId);
addFileToWb(wbs, wb, fi, clean, true);
}
}
@Override
public void sendFileToWb(final BaseFileItem fi, boolean clean) {
sendFileToWb(null, fi, clean);
}
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;
}
undoList
.computeIfAbsent(wbId, id -> new LimitedLinkedList<>())
.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.WML_FILE);
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(wb.getAddJson()).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;
}
}