/*
* 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.publisher;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.atmosphere.cpr.AtmosphereResource;
import org.atmosphere.cpr.AtmosphereResourceEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zodiark.protocol.Envelope;
import org.zodiark.protocol.Message;
import org.zodiark.protocol.Paths;
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.Error;
import org.zodiark.service.RetrieveMessage;
import org.zodiark.service.Session;
import org.zodiark.service.db.result.ModeId;
import org.zodiark.service.db.result.ShowId;
import org.zodiark.service.db.result.Status;
import org.zodiark.service.state.EndpointState;
import org.zodiark.service.subscriber.SubscriberEndpoint;
import org.zodiark.service.wowza.WowzaUUID;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import static org.atmosphere.cpr.AtmosphereResourceEventListenerAdapter.OnDisconnect;
import static org.zodiark.protocol.Paths.BROADCASTER_CREATE;
import static org.zodiark.protocol.Paths.DB_ENDPOINT_STATE;
import static org.zodiark.protocol.Paths.DB_GET_WORD_PASSTHROUGH;
import static org.zodiark.protocol.Paths.DB_POST_PUBLISHER_ONDEMAND_END;
import static org.zodiark.protocol.Paths.DB_POST_PUBLISHER_ONDEMAND_START;
import static org.zodiark.protocol.Paths.DB_POST_PUBLISHER_SESSION_CREATE;
import static org.zodiark.protocol.Paths.DB_PUBLISHER_ACTIONS;
import static org.zodiark.protocol.Paths.DB_PUBLISHER_AVAILABLE_ACTIONS_PASSTHROUGHT;
import static org.zodiark.protocol.Paths.DB_PUBLISHER_ERROR_REPORT;
import static org.zodiark.protocol.Paths.DB_PUBLISHER_LOAD_CONFIG;
import static org.zodiark.protocol.Paths.DB_PUBLISHER_LOAD_CONFIG_ERROR_PASSTHROUGHT;
import static org.zodiark.protocol.Paths.DB_PUBLISHER_LOAD_CONFIG_GET;
import static org.zodiark.protocol.Paths.DB_PUBLISHER_PUBLIC_MODE;
import static org.zodiark.protocol.Paths.DB_PUBLISHER_PUBLIC_MODE_END;
import static org.zodiark.protocol.Paths.DB_PUBLISHER_SAVE_CONFIG;
import static org.zodiark.protocol.Paths.DB_PUBLISHER_SAVE_CONFIG_PUT;
import static org.zodiark.protocol.Paths.DB_PUBLISHER_SETTINGS_SHOW;
import static org.zodiark.protocol.Paths.DB_PUBLISHER_SETTINGS_SHOW_GET_PASSTHROUGHT;
import static org.zodiark.protocol.Paths.DB_PUBLISHER_SETTINGS_SHOW_SAVE;
import static org.zodiark.protocol.Paths.DB_PUBLISHER_SHARED_PRIVATE_END;
import static org.zodiark.protocol.Paths.DB_PUBLISHER_SHARED_PRIVATE_START;
import static org.zodiark.protocol.Paths.DB_PUBLISHER_SHARED_PRIVATE_START_POST;
import static org.zodiark.protocol.Paths.DB_PUBLISHER_SHOW_END;
import static org.zodiark.protocol.Paths.DB_PUBLISHER_SHOW_START;
import static org.zodiark.protocol.Paths.DB_PUBLISHER_SUBSCRIBER_PROFILE_GET_PASSTHROUGH;
import static org.zodiark.protocol.Paths.DB_PUBLISHER_SUBSCRIBER_PROFILE_PUT;
import static org.zodiark.protocol.Paths.DB_SUBSCRIBER_BLOCK;
import static org.zodiark.protocol.Paths.DB_SUBSCRIBER_EJECT;
import static org.zodiark.protocol.Paths.ERROR_STREAMING_SESSION;
import static org.zodiark.protocol.Paths.FAILED_PUBLISHER_STREAMING_SESSION;
import static org.zodiark.protocol.Paths.PUBLISHER_ABOUT_READY;
import static org.zodiark.protocol.Paths.RETRIEVE_PUBLISHER;
import static org.zodiark.protocol.Paths.RETRIEVE_SUBSCRIBER;
import static org.zodiark.protocol.Paths.VALIDATE_PUBLISHER_STREAMING_SESSION;
import static org.zodiark.protocol.Paths.WOWZA_CONNECT;
/**
* The Publisher's application logic for validating, creating and starting a {@link org.zodiark.service.session.StreamingSession}
*/
@On(Paths.SERVICE_PUBLISHER)
public class PublisherServiceImpl implements PublisherService, Session<PublisherEndpoint> {
private final ConcurrentHashMap<String, PublisherEndpoint> endpoints = new ConcurrentHashMap<>();
private final Logger logger = LoggerFactory.getLogger(PublisherServiceImpl.class);
@Inject
public EventBus eventBus;
@Inject
public ObjectMapper mapper;
@Inject
public Context context;
private EndpointUtils<PublisherEndpoint> utils;
@PostConstruct
public void init() {
utils = new EndpointUtils(eventBus, mapper, endpoints);
}
@Override
public void reactTo(Envelope e, AtmosphereResource r, Reply reply) {
logger.trace("Handling Publisher Envelop {} to Service {}", e, r.uuid());
String path = e.getMessage().getPath();
switch (path) {
case DB_POST_PUBLISHER_SESSION_CREATE:
createSession(e, r);
break;
case VALIDATE_PUBLISHER_STREAMING_SESSION:
createOrJoinStreamingSession(e, r);
break;
case DB_PUBLISHER_SHOW_START:
startStreamingSession(e, r);
break;
case DB_PUBLISHER_ERROR_REPORT:
reportError(e, r);
break;
case FAILED_PUBLISHER_STREAMING_SESSION:
errorStreamingSession(e);
break;
case DB_PUBLISHER_SHOW_END:
terminateStreamingSession(e, r);
break;
case DB_PUBLISHER_SAVE_CONFIG:
saveConfig(e, r);
break;
case DB_PUBLISHER_SETTINGS_SHOW:
loadOrSaveShow(e, r);
break;
case DB_POST_PUBLISHER_ONDEMAND_START:
onDemandStart(e, r);
break;
case DB_POST_PUBLISHER_ONDEMAND_END:
onDemandEnd(e, r);
break;
case DB_GET_WORD_PASSTHROUGH:
getMotd(e, r);
break;
case DB_PUBLISHER_SUBSCRIBER_PROFILE_GET_PASSTHROUGH:
getSubscriberProfile(path, e);
break;
case DB_PUBLISHER_SUBSCRIBER_PROFILE_PUT:
updateSubscriberProfile(path, e);
break;
case DB_PUBLISHER_SHARED_PRIVATE_START:
sharedPrivateSession(e, r);
break;
case DB_PUBLISHER_SHARED_PRIVATE_END:
endSharedPrivateSession(e, r);
break;
case DB_SUBSCRIBER_EJECT:
case DB_SUBSCRIBER_BLOCK:
validateAndStatusEvent(path, e);
break;
case DB_PUBLISHER_PUBLIC_MODE:
case DB_PUBLISHER_PUBLIC_MODE_END:
publisherPublicMode(path, e);
break;
case DB_PUBLISHER_ACTIONS:
savesAction(path, e);
break;
default:
throw new IllegalStateException("Invalid Message Path " + path);
}
}
private void savesAction(String path, Envelope e) {
utils.statusEvent(path, e);
}
private void publisherPublicMode(final String path, final Envelope e) {
final PublisherEndpoint p = utils.retrieve(e.getUuid());
if (!utils.validateAll(p, e)) ;
utils.statusEvent(path, e, p, new Reply<Status, String>() {
@Override
public void ok(Status status) {
logger.trace("Status {}", status);
String[] pathSegments = path.split("/");
if (pathSegments[6].endsWith("start")) {
p.state().modeId(ModeId.PUBLIC);
} else {
p.state().modeId(ModeId.VOID);
}
response(e, p, utils.constructMessage(path, utils.writeAsString(status), e.getMessage().getUUID()));
}
@Override
public void fail(ReplyException replyException) {
error(e, p, utils.constructMessage(path, utils.writeAsString(new org.zodiark.service.Error().error("Unauthorized")), e.getMessage().getUUID()));
}
});
}
private void validateAndStatusEvent(String path, Envelope e) {
final PublisherEndpoint p = utils.retrieve(e.getUuid());
if (!utils.validate(p, e)) return;
String[] paths = e.getMessage().getPath().split("/");
boolean subscriberOk = validateSubscriberState(paths[5], p);
// TODO: utils.validate Subscriber
// if (!isValid.get()) {
// error(e, p, utils.constructMessage("/error", "No Subscriber for " + paths[5]));
// }
utils.statusEvent(path, e);
}
private boolean validateSubscriberState(String subscriberId, final PublisherEndpoint p) {
final AtomicBoolean isValid = new AtomicBoolean();
// DAangerous if the path change.
eventBus.message(RETRIEVE_SUBSCRIBER, subscriberId, new Reply<SubscriberEndpoint, String>() {
@Override
public void ok(SubscriberEndpoint s) {
isValid.set(s.publisherEndpoint().equals(p));
}
@Override
public void fail(ReplyException replyException) {
logger.error("No Endpoint");
}
});
return isValid.get();
}
private void endSharedPrivateSession(Envelope e, AtmosphereResource r) {
utils.statusEvent(DB_PUBLISHER_SHARED_PRIVATE_END, e);
}
private void sharedPrivateSession(Envelope e, AtmosphereResource r) {
utils.statusEvent(DB_PUBLISHER_SHARED_PRIVATE_START_POST, e);
}
private void getSubscriberProfile(String path, Envelope e) {
utils.passthroughEvent(path, e);
}
private void updateSubscriberProfile(String path, Envelope e) {
String[] paths = path.split("/");
PublisherEndpoint p = utils.retrieve(e.getUuid());
if (!utils.validate(p, e)) return;
// TODO: utils.validate Subscriber
boolean subscriberOk = validateSubscriberState(paths[5], p);
utils.statusEvent(DB_PUBLISHER_SUBSCRIBER_PROFILE_PUT, e, p);
}
private void getMotd(Envelope e, AtmosphereResource r) {
utils.passthroughEvent(DB_GET_WORD_PASSTHROUGH, e);
}
private void onDemandEnd(Envelope e, AtmosphereResource r) {
utils.statusEvent(DB_POST_PUBLISHER_ONDEMAND_END, e);
}
private void onDemandStart(Envelope e, AtmosphereResource r) {
utils.statusEvent(DB_POST_PUBLISHER_ONDEMAND_START, e);
}
public void saveConfig(final Envelope e, AtmosphereResource r) {
utils.statusEvent(DB_PUBLISHER_SAVE_CONFIG_PUT, e);
}
public void loadOrSaveShow(final Envelope e, AtmosphereResource r) {
Message m = e.getMessage();
if (!m.hasData()) {
utils.passthroughEvent(DB_PUBLISHER_SETTINGS_SHOW_GET_PASSTHROUGHT, e);
} else {
utils.statusEvent(DB_PUBLISHER_SETTINGS_SHOW_SAVE, e);
}
}
public void reportError(final Envelope e, AtmosphereResource r) {
utils.statusEvent(DB_PUBLISHER_ERROR_REPORT, e);
}
/**
* {@inheritDoc}
*/
@Override
public void retrieveEndpoint(Object uuid, Reply reply) {
if (String.class.isAssignableFrom(uuid.getClass())) {
PublisherEndpoint p = endpoints.get(uuid.toString());
if (p != null) {
reply.ok(p);
return;
}
}
reply.fail(ReplyException.DEFAULT);
}
/**
* {@inheritDoc}
*/
@Override
public void errorStreamingSession(Envelope e) {
try {
PublisherResults result = mapper.readValue(e.getMessage().getData(), PublisherResults.class);
PublisherEndpoint p = endpoints.get(result.getUuid());
Message m = new Message();
m.setPath(ERROR_STREAMING_SESSION);
try {
m.setData(mapper.writeValueAsString(new PublisherResults("ERROR")));
} catch (JsonProcessingException e1) {
//
}
error(e, p, m);
} catch (IOException e1) {
logger.warn("{}", e1);
}
}
/**
* {@inheritDoc}
*/
@Override
public void terminateStreamingSession(final Envelope e, AtmosphereResource r) {
final PublisherEndpoint p = utils.retrieve(e.getUuid());
if (!utils.validate(p, e)) return;
utils.statusEvent(DB_PUBLISHER_SHOW_END.replace("{showId}", String.valueOf(p.state().showId().showId())), e);
}
/**
* {@inheritDoc}
*/
public void createOrJoinStreamingSession(final Envelope e, AtmosphereResource r) {
final PublisherEndpoint p = utils.retrieve(e.getUuid());
if (!utils.validate(p, e)) return;
try {
p.wowzaServerUUID(mapper.readValue(e.getMessage().getData(), WowzaUUID.class).getUuid());
} catch (IOException e1) {
logger.warn("{}", e1);
}
// TODO: Callback is not called at the moment as the dispatching to Wowza is asynchronous
eventBus.message(WOWZA_CONNECT, new RetrieveMessage(p.uuid(), e.getMessage()), new Reply<String, String>() {
@Override
public void ok(String uuid) {
// TODO: Proper Message
Message m = new Message();
response(e, p, m);
}
@Override
public void fail(ReplyException replyException) {
error(e, p, utils.constructMessage(WOWZA_CONNECT, "error", e.getMessage().getUUID()));
}
});
}
/**
* {@inheritDoc}
*/
@Override
public void startStreamingSession(final Envelope e, AtmosphereResource r) {
String uuid = e.getUuid();
final PublisherEndpoint p = utils.retrieve(uuid);
if (!utils.validate(p, e)) return;
eventBus.message(DB_PUBLISHER_SHOW_START, new RetrieveMessage(p.uuid(), e.getMessage()), new Reply<ShowId, String>() {
@Override
public void ok(ShowId showId) {
logger.trace("Publisher ready {}", p);
p.state().showId(showId);
response(e, p, utils.constructMessage(DB_PUBLISHER_SHOW_START, utils.writeAsString(p.state().showId()), e.getMessage().getUUID()));
}
@Override
public void fail(ReplyException replyException) {
// TODO: Wrong error message
error(e, p, utils.constructMessage(DB_PUBLISHER_SHOW_START, utils.writeAsString(new Error().error("Unauthorized")), e.getMessage().getUUID()));
}
}).message(BROADCASTER_CREATE, p);
}
@Override
public void response(Envelope e, PublisherEndpoint endpoint, Message m) {
utils.response(e, endpoint, m);
}
/**
* {@inheritDoc}
*/
@Override
public void reactTo(String path, Object message, Reply reply) {
switch (path) {
case RETRIEVE_PUBLISHER:
retrieveEndpoint(message, reply);
break;
case PUBLISHER_ABOUT_READY:
resetEndpoint(message, reply);
break;
default:
logger.error("Can't react to {}", path);
}
}
/**
* {@inheritDoc}
*/
@Override
public void resetEndpoint(Object publisherEndpointUuid, Reply reply) {
try {
PublisherEndpoint p = endpoints.get(publisherEndpointUuid.toString());
AtmosphereResource r = p.resource();
Message m = new Message();
m.setPath(PUBLISHER_ABOUT_READY);
m.setData(mapper.writeValueAsString(new PublisherResults("READY")));
Envelope e = Envelope.newPublisherRequest(p.uuid(), m);
r.write(mapper.writeValueAsString(e));
} catch (JsonProcessingException e1) {
logger.debug("Unable to write {} {}", publisherEndpointUuid);
reply.fail(ReplyException.DEFAULT);
}
}
/**
* {@inheritDoc}
*/
@Override
public PublisherEndpoint createSession(final Envelope e, final AtmosphereResource r) {
if (!e.getMessage().hasData()) {
error(e, r, utils.errorMessage("error", e.getMessage().getUUID()));
return null;
}
final String uuid = e.getUuid();
PublisherEndpoint p = endpoints.get(uuid);
if (p == null) {
p = context.newInstance(PublisherEndpoint.class);
p.uuid(uuid).resource(r);
endpoints.put(uuid, p);
final AtomicReference<PublisherEndpoint> publisher = new AtomicReference<>(p);
String data = e.getMessage().getData();
e.getMessage().setData(injectIp(r.getRequest().getRemoteAddr(), data));
eventBus.message(DB_POST_PUBLISHER_SESSION_CREATE, new RetrieveMessage(p.uuid(), e.getMessage()), new Reply<Status, String>() {
@Override
public void ok(final Status status) {
final PublisherEndpoint p = publisher.get();
logger.trace("{} succeed for {}", DB_POST_PUBLISHER_SESSION_CREATE, p);
eventBus.message(DB_ENDPOINT_STATE, new RetrieveMessage(p.uuid(), e.getMessage()), new Reply<EndpointState, String>() {
@Override
public void ok(EndpointState state) {
p.state(state);
eventBus.message(DB_PUBLISHER_AVAILABLE_ACTIONS_PASSTHROUGHT, new RetrieveMessage(p.uuid(), e.getMessage()), new Reply<String, String>() {
@Override
public void ok(String passthrough) {
utils.succesPassThrough(e, p, DB_PUBLISHER_AVAILABLE_ACTIONS_PASSTHROUGHT, passthrough);
eventBus.message(DB_PUBLISHER_LOAD_CONFIG_GET, new RetrieveMessage(p.uuid(), e.getMessage()), new Reply<String, String>() {
@Override
public void ok(String passthrough) {
utils.succesPassThrough(e, p, DB_PUBLISHER_LOAD_CONFIG, passthrough);
utils.passthroughEvent(DB_PUBLISHER_LOAD_CONFIG_ERROR_PASSTHROUGHT, e, p);
}
@Override
public void fail(ReplyException replyException) {
utils.failPassThrough(e, p, replyException);
}
});
}
@Override
public void fail(ReplyException replyException) {
utils.failPassThrough(e, p, replyException);
}
});
}
@Override
public void fail(ReplyException replyException) {
error(e, publisher.get(), utils.errorMessage("error", e.getMessage().getUUID()));
}
});
}
@Override
public void fail(ReplyException replyException) {
error(e, publisher.get(), utils.constructMessage(DB_POST_PUBLISHER_SESSION_CREATE, "error", e.getMessage().getUUID()));
}
});
}
r.addEventListener(new OnDisconnect() {
@Override
public void onDisconnect(AtmosphereResourceEvent event) {
logger.debug("Publisher {} disconnected", uuid);
endpoints.remove(uuid);
}
});
return p;
}
private String injectIp(String remoteAddr, String data) {
return data.replace("ip\":\"\"", "ip\":\"" + remoteAddr + "\"");
}
@Override
public void error(Envelope e, PublisherEndpoint endpoint, Message m) {
utils.error(e, endpoint, m);
}
@Override
public void error(Envelope e, AtmosphereResource r, Message m) {
utils.error(e, r, m);
}
}