/*
* Copyright 2013-2014 High-Level Technologies
*
* Licensed 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.zodiark.service.subscriber;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.atmosphere.cpr.AtmosphereResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zodiark.protocol.Envelope;
import org.zodiark.protocol.Message;
import org.zodiark.server.Context;
import org.zodiark.server.EventBus;
import org.zodiark.server.Reply;
import org.zodiark.server.ReplyException;
import org.zodiark.server.annotation.On;
import org.zodiark.service.EndpointUtils;
import org.zodiark.service.RetrieveMessage;
import org.zodiark.service.Session;
import org.zodiark.service.db.result.Action;
import org.zodiark.service.db.result.ActionState;
import org.zodiark.service.db.result.Actions;
import org.zodiark.service.db.result.FavoriteId;
import org.zodiark.service.db.result.TransactionId;
import org.zodiark.service.publisher.PublisherEndpoint;
import org.zodiark.service.session.StreamingRequest;
import org.zodiark.service.state.EndpointState;
import org.zodiark.service.util.UUID;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import static org.zodiark.protocol.Paths.BEGIN_SUBSCRIBER_STREAMING_SESSION;
import static org.zodiark.protocol.Paths.BROADCASTER_TRACK;
import static org.zodiark.protocol.Paths.DB_ENDPOINT_STATE;
import static org.zodiark.protocol.Paths.DB_POST_SUBSCRIBER_SESSION_CREATE;
import static org.zodiark.protocol.Paths.DB_SUBSCRIBER_AVAILABLE_ACTIONS;
import static org.zodiark.protocol.Paths.DB_SUBSCRIBER_EXTRA;
import static org.zodiark.protocol.Paths.DB_SUBSCRIBER_FAVORITES_END;
import static org.zodiark.protocol.Paths.DB_SUBSCRIBER_FAVORITES_START;
import static org.zodiark.protocol.Paths.DB_SUBSCRIBER_JOIN_ACTION;
import static org.zodiark.protocol.Paths.DB_SUBSCRIBER_REQUEST_ACTION;
import static org.zodiark.protocol.Paths.ERROR_STREAMING_SESSION;
import static org.zodiark.protocol.Paths.FAILED_SUBSCRIBER_STREAMING_SESSION;
import static org.zodiark.protocol.Paths.JOIN_SUBSCRIBER_STREAMING_SESSION;
import static org.zodiark.protocol.Paths.MONITOR_RESOURCE;
import static org.zodiark.protocol.Paths.RETRIEVE_PUBLISHER;
import static org.zodiark.protocol.Paths.RETRIEVE_SUBSCRIBER;
import static org.zodiark.protocol.Paths.SERVICE_SUBSCRIBER;
import static org.zodiark.protocol.Paths.SUBSCRIBER_BROWSER_HANDSHAKE;
import static org.zodiark.protocol.Paths.SUBSCRIBER_BROWSER_HANDSHAKE_OK;
import static org.zodiark.protocol.Paths.TERMINATE_SUBSCRIBER_STREAMING_SESSSION;
/**
* A Service responsible for managing {@link SubscriberEndpoint}
*/
@On(SERVICE_SUBSCRIBER)
public class SubscriberServiceImpl implements SubscriberService, Session<SubscriberEndpoint> {
private final ConcurrentHashMap<String, SubscriberEndpoint> endpoints = new ConcurrentHashMap<>();
private final Logger logger = LoggerFactory.getLogger(SubscriberServiceImpl.class);
@Inject
public Context context;
@Inject
public EventBus eventBus;
@Inject
public ObjectMapper mapper;
@Inject
public StreamingRequest requestClass;
private EndpointUtils<SubscriberEndpoint> utils;
@PostConstruct
public void init() {
utils = new EndpointUtils(eventBus, mapper, endpoints);
}
@Override
public void reactTo(Envelope e, AtmosphereResource r, Reply reply) {
logger.trace("Handling Subscriber Envelop {} to Service {}", e, r.uuid());
String path = e.getMessage().getPath();
switch (path) {
case DB_POST_SUBSCRIBER_SESSION_CREATE:
createSession(e, r);
break;
case JOIN_SUBSCRIBER_STREAMING_SESSION:
startStreamingSession(e, r);
break;
case FAILED_SUBSCRIBER_STREAMING_SESSION:
errorStreamingSession(e);
break;
case TERMINATE_SUBSCRIBER_STREAMING_SESSSION:
terminateStreamingSession(e, r);
break;
case SUBSCRIBER_BROWSER_HANDSHAKE:
connectEndpoint(e, r);
break;
case DB_SUBSCRIBER_AVAILABLE_ACTIONS:
availableActions(e, r);
break;
case DB_SUBSCRIBER_REQUEST_ACTION:
requestAction(e, r);
break;
case DB_SUBSCRIBER_JOIN_ACTION:
joinAction(e, r);
break;
case DB_SUBSCRIBER_FAVORITES_END:
deleteFavorite(e, r);
break;
case DB_SUBSCRIBER_FAVORITES_START:
addFavorites(e, r);
break;
case DB_SUBSCRIBER_EXTRA:
tipPublisher(e, r);
break;
default:
throw new IllegalStateException("Invalid Message Path" + e.getMessage().getPath());
}
}
private void tipPublisher(final Envelope e, AtmosphereResource r) {
final SubscriberEndpoint s = utils.retrieve(e.getUuid());
if (!utils.validate(s, e)) return;
if (!s.hasSession()) {
error(e, r, new Message().setPath("/error").setData("unauthorized"));
return;
}
eventBus.message(DB_SUBSCRIBER_EXTRA, new RetrieveMessage(s.uuid(), e.getMessage()), new Reply<TransactionId, String>() {
@Override
public void ok(TransactionId success) {
s.transactionId(success);
logger.debug("Action Accepted for {}", s);
response(e, s, utils.constructMessage(DB_SUBSCRIBER_EXTRA, utils.writeAsString(success), e.getMessage().getUUID()));
}
@Override
public void fail(ReplyException replyException) {
error(e, s, utils.errorMessage("error", e.getMessage().getUUID()));
}
});
}
private void addFavorites(final Envelope e, AtmosphereResource r) {
final SubscriberEndpoint s = utils.retrieve(e.getUuid());
if (!utils.validate(s, e) || !s.hasSession()) return;
eventBus.message(DB_SUBSCRIBER_FAVORITES_START, new RetrieveMessage(s.uuid(), e.getMessage()), new Reply<FavoriteId, String>() {
@Override
public void ok(FavoriteId success) {
logger.debug("Favorite {}", success);
response(e, s, utils.constructMessage(DB_SUBSCRIBER_FAVORITES_START, utils.writeAsString(success), e.getMessage().getUUID()));
}
@Override
public void fail(ReplyException replyException) {
error(e, s, utils.errorMessage("error", e.getMessage().getUUID()));
}
});
}
private void deleteFavorite(Envelope e, AtmosphereResource r) {
SubscriberEndpoint s = utils.retrieve(e.getUuid());
// TODO: Should we keep favorites in memory? JFA -> No
if (!utils.validate(s, e) || !s.hasSession()) return;
utils.statusEvent(DB_SUBSCRIBER_FAVORITES_END, e, s);
}
private void joinAction(final Envelope e, AtmosphereResource r) {
final SubscriberEndpoint s = utils.retrieve(e.getUuid());
if (!utils.validate(s, e)) return;
if (!s.hasSession() || !s.actionRequested()) {
error(e, r, new Message().setPath("/error").setData("unauthorized"));
}
eventBus.message(DB_SUBSCRIBER_JOIN_ACTION, new RetrieveMessage(s.uuid(), e.getMessage()), new Reply<TransactionId, String>() {
@Override
public void ok(TransactionId success) {
s.transactionId(success);
logger.debug("Action Accepted for {}", s);
response(e, s, utils.constructMessage(DB_SUBSCRIBER_JOIN_ACTION, utils.writeAsString(s.transactionId()), e.getMessage().getUUID()));
}
@Override
public void fail(ReplyException replyException) {
error(e, s, utils.errorMessage("error", e.getMessage().getUUID()));
}
});
}
private void requestAction(final Envelope e, AtmosphereResource r) {
final SubscriberEndpoint s = utils.retrieve(e.getUuid());
if (!utils.validate(s, e)) return;
if (!s.hasSession() || !s.actionRequested()) {
error(e, r, new Message().setPath("/error").setData("unauthorized"));
}
// (1) Load DB_SUBSCRIBER_REQUEST_ACTION in memory
// (2) Wait for Publisher's response. Response is asynchronous until the Publisher disconnect or change state.
// (3) OK => Public => Subscriber's chat(facturation) scramble = false, maxDuration =0
// (4) Ok => Public avec Timer scramble = false, maxDuration = XXX
// (5) OK => scramble = true, maxDuration = 0 ..... (Ping le Publisher pour min et max duration)
// Facturation plus tardive pour les suivants
// LA DB peut renvoyée une erreur.
boolean error = false;
Action requestedAction = null;
try {
requestedAction = mapper.readValue(e.getMessage().getData(), Action.class);
} catch (IOException e1) {
logger.error("{}", e1);
error = true;
}
List<Action> actions = s.actionsAvailable().getActions();
if (error || actions == null || actions.isEmpty() || !actions.contains(requestedAction)) {
error(e, s, utils.errorMessage("error", e.getMessage().getUUID()));
}
// Subscriber will be deleted in case an error happens.
s.actionRequested(true);
eventBus.message(DB_SUBSCRIBER_REQUEST_ACTION, new RetrieveMessage(s.uuid(), e.getMessage()), new Reply<ActionState, String>() {
@Override
public void ok(ActionState state) {
logger.debug("Action Accepted for {}", state);
response(e, s, utils.constructMessage(DB_SUBSCRIBER_REQUEST_ACTION, utils.writeAsString(state),e.getMessage().getUUID()));
}
@Override
public void fail(ReplyException replyException) {
error(e, s, utils.errorMessage("error",e.getMessage().getUUID()));
}
});
}
@Override
public void reactTo(String path, Object message, Reply reply) {
switch (path) {
case RETRIEVE_SUBSCRIBER:
retrieveEndpoint(message, reply);
break;
}
}
private SubscriberEndpoint createEndpoint(AtmosphereResource resource, Message m) {
SubscriberEndpoint s = context.newInstance(SubscriberEndpoint.class);
s.uuid(resource.uuid()).resource(resource);
eventBus.message(BROADCASTER_TRACK, s).message(MONITOR_RESOURCE, resource);
return s;
}
private void message(String path, SubscriberEndpoint s, Message m) {
eventBus.message(path, new RetrieveMessage(s.uuid(), m));
}
@Override
public SubscriberEndpoint createSession(final Envelope e, AtmosphereResource r) {
String uuid = e.getUuid();
SubscriberEndpoint s = endpoints.get(uuid);
if (s == null || !s.hasSession()) {
s = createEndpoint(r, e.getMessage());
endpoints.put(uuid, s);
final AtomicReference<SubscriberEndpoint> subscriberEndpoint = new AtomicReference<>(s);
eventBus.message(DB_ENDPOINT_STATE, new RetrieveMessage(s.uuid(), e.getMessage()), new Reply<EndpointState, String>() {
@Override
public void ok(EndpointState state) {
SubscriberEndpoint s = subscriberEndpoint.get();
// Subscriber will be deleted in case an error happens.
s.hasSession(true).state(state).publisherEndpoint(retrieve(state.publisherUUID()));
utils.statusEvent(DB_POST_SUBSCRIBER_SESSION_CREATE, e, s);
}
@Override
public void fail(ReplyException replyException) {
error(e, subscriberEndpoint.get(), utils.constructMessage(DB_ENDPOINT_STATE, "error", e.getMessage().getUUID()));
}
});
} else {
error(e, r, new Message().setPath("/error").setData("unauthorized"));
}
return s;
}
public PublisherEndpoint retrieve(final String uuid) {
// TODO: This won't work asynchronous
final AtomicReference<PublisherEndpoint> publisher = new AtomicReference<>(null);
eventBus.message(RETRIEVE_PUBLISHER, uuid, new Reply<PublisherEndpoint, String>() {
@Override
public void ok(PublisherEndpoint p) {
publisher.set(p);
}
@Override
public void fail(ReplyException replyException) {
logger.error("Unable to retrieve publisher {}", uuid);
}
});
return publisher.get();
}
private void availableActions(final Envelope e, AtmosphereResource r) {
final SubscriberEndpoint s = utils.retrieve(e.getUuid());
if (!utils.validate(s, e)) return;
if (!s.hasSession()) {
error(e, r, new Message().setPath("/error").setData("unauthorized"));
}
// Subscriber will be deleted in case an error happens.
s.actionRequested(true);
eventBus.message(DB_SUBSCRIBER_AVAILABLE_ACTIONS, new RetrieveMessage(s.uuid(), e.getMessage()), new Reply<Actions, String>() {
@Override
public void ok(Actions actions) {
s.actionsAvailable(actions);
response(e, s, utils.constructMessage(DB_SUBSCRIBER_AVAILABLE_ACTIONS, utils.writeAsString(actions), e.getMessage().getUUID()));
}
@Override
public void fail(ReplyException replyException) {
error(e, s, utils.errorMessage("error",e.getMessage().getUUID()));
}
});
}
@Override
public void error(Envelope e, SubscriberEndpoint endpoint, Message m) {
utils.error(e, endpoint, m);
}
@Override
public void error(Envelope e, AtmosphereResource r, Message m) {
utils.error(e, r, m);
}
@Override
public void connectEndpoint(Envelope e, AtmosphereResource r) {
logger.info("Subscriber Connected {}", e);
SubscriberEndpoint s = createEndpoint(r, e.getMessage());
response(e, s, utils.constructMessage(SUBSCRIBER_BROWSER_HANDSHAKE_OK, "OK", e.getMessage().getUUID()));
}
// TODO: Remove
@Override
public void errorStreamingSession(Envelope e) {
try {
SubscriberResults result = mapper.readValue(e.getMessage().getData(), SubscriberResults.class);
SubscriberEndpoint p = endpoints.get(result.getUuid());
error(e, p, utils.constructMessage(ERROR_STREAMING_SESSION, "error", e.getMessage().getUUID()));
} catch (IOException e1) {
logger.warn("{}", e1);
}
}
@Override
public void terminateStreamingSession(final Envelope e, AtmosphereResource r) {
String uuid = e.getMessage().getUUID();
SubscriberEndpoint p = endpoints.get(uuid);
}
@Override
public void retrieveEndpoint(Object subscriberUuid, Reply reply) {
if (String.class.isAssignableFrom(subscriberUuid.getClass())) {
SubscriberEndpoint s = endpoints.get(subscriberUuid.toString());
if (s != null) {
reply.ok(s);
} else {
reply.fail(ReplyException.DEFAULT);
}
} else {
reply.fail(ReplyException.DEFAULT);
}
}
@Override
public void startStreamingSession(final Envelope e, AtmosphereResource r) {
UUID uuid = null;
try {
uuid = mapper.readValue(e.getMessage().getData(), UUID.class);
} catch (IOException e1) {
logger.warn("{}", e1);
}
final SubscriberEndpoint s = utils.retrieve(uuid.getUuid());
eventBus.message(BEGIN_SUBSCRIBER_STREAMING_SESSION, new RetrieveMessage(s.uuid(), e.getMessage()), new Reply<RetrieveMessage, String>() {
@Override
public void ok(RetrieveMessage ok) {
response(e, s, utils.constructMessage(BEGIN_SUBSCRIBER_STREAMING_SESSION, "OK", e.getMessage().getUUID()));
}
@Override
public void fail(ReplyException replyException) {
error(e, s, utils.constructMessage(BEGIN_SUBSCRIBER_STREAMING_SESSION, "error", e.getMessage().getUUID()));
}
});
}
@Override
public void response(Envelope e, SubscriberEndpoint endpoint, Message m) {
utils.response(e, endpoint, m);
}
}