package org.dyndns.jkiddo;
import java.awt.AWTException;
import java.awt.BorderLayout;
import java.awt.Button;
import java.awt.Dialog;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.MenuItem;
import java.awt.PopupMenu;
import java.awt.SystemTray;
import java.awt.TextArea;
import java.awt.TrayIcon;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.net.URI;
import java.net.URL;
import java.util.Collection;
import java.util.EnumSet;
import java.util.Map;
import java.util.Set;
import javax.servlet.DispatcherType;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import org.dyndns.jkiddo.Jolivia.JoliviaBuilder.SecurityScheme;
import org.dyndns.jkiddo.dmap.chunks.audio.SongAlbum;
import org.dyndns.jkiddo.dmap.chunks.audio.SongArtist;
import org.dyndns.jkiddo.dmp.chunks.media.AuthenticationMethod.PasswordMethod;
import org.dyndns.jkiddo.dmp.chunks.media.ItemName;
import org.dyndns.jkiddo.dmp.chunks.media.Listing;
import org.dyndns.jkiddo.dmp.chunks.media.ListingItem;
import org.dyndns.jkiddo.dmp.model.MediaItem;
import org.dyndns.jkiddo.dmp.util.DmapUtil;
import org.dyndns.jkiddo.guice.JoliviaServer;
import org.dyndns.jkiddo.jetty.DmapConnectionFactory;
import org.dyndns.jkiddo.logic.interfaces.IImageStoreReader;
import org.dyndns.jkiddo.logic.interfaces.IMusicStoreReader;
import org.dyndns.jkiddo.raop.ISpeakerListener;
import org.dyndns.jkiddo.raop.server.IPlayingInformation;
import org.dyndns.jkiddo.service.daap.client.IClientSessionListener;
import org.dyndns.jkiddo.service.daap.client.Session;
import org.eclipse.jetty.security.Authenticator;
import org.eclipse.jetty.security.ConstraintMapping;
import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.security.HashLoginService;
import org.eclipse.jetty.security.SecurityHandler;
import org.eclipse.jetty.security.authentication.BasicAuthenticator;
import org.eclipse.jetty.security.authentication.DigestAuthenticator;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.util.security.Credential;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Preconditions;
import com.google.inject.servlet.GuiceFilter;
public class Jolivia {
private static final Logger LOGGER = LoggerFactory.getLogger(Jolivia.class);
private static final String AboutMessage = "Use it only to be disruptive";
public static class JoliviaBuilder {
private Integer port = 3689;
private Integer airplayPort = 5000;
private Integer pairingCode = 1337;
private String name = "Jolivia";
private ISpeakerListener speakerListener;
private IClientSessionListener clientSessionListener = new DefaultClientSessionListener();
private IMusicStoreReader musicStoreReader = new IMusicStoreReader() {
@Override
public void readTunesMemoryOptimized(final Listing listing, final Map<Long, String> map) throws Exception {
// TODO Auto-generated method stub
}
@Override
public Collection<MediaItem> readTunes() throws Exception {
// TODO Auto-generated method stub
return null;
}
@Override
public URI getTune(final String tuneIdentifier) throws Exception {
// TODO Auto-generated method stub
return null;
}
};
private IImageStoreReader imageStoreReader = new IImageStoreReader() {
@Override
public Set<IImageItem> readImages() throws Exception {
// TODO Auto-generated method stub
return null;
}
@Override
public byte[] getImageThumb(final IImageItem image) throws Exception {
// TODO Auto-generated method stub
return null;
}
@Override
public URI getImage(final IImageItem image) throws Exception {
// TODO Auto-generated method stub
return null;
}
};
private IPlayingInformation iplayingInformation = new DefaultIPlayingInformation();
private PasswordMethod security = PasswordMethod.NO_PASSWORD;
private SecurityScheme scheme;
private String appleUsername;
private String applePassword;
public JoliviaBuilder port(final int port) {
this.port = port;
return this;
}
public JoliviaBuilder homeSharing(final String appleUsername, final String applePassword) {
this.appleUsername = appleUsername;
this.applePassword = applePassword;
return this;
}
public JoliviaBuilder pairingCode(final int pairingCode) {
this.pairingCode = pairingCode;
return this;
}
public JoliviaBuilder airplayPort(final int airplayPort) {
this.airplayPort = airplayPort;
return this;
}
public JoliviaBuilder name(final String name) {
this.name = name;
return this;
}
public enum SecurityScheme {
DIGEST, BASIC
}
public JoliviaBuilder security(final PasswordMethod security, final SecurityScheme scheme) {
this.scheme = scheme;
this.security = security;
return this;
}
public JoliviaBuilder musicStoreReader(final IMusicStoreReader musicStoreReader) {
this.musicStoreReader = musicStoreReader;
return this;
}
public JoliviaBuilder imageStoreReader(final IImageStoreReader imageStoreReader) {
this.imageStoreReader = imageStoreReader;
return this;
}
public JoliviaBuilder playingInformation(final IPlayingInformation iplayingInformation) {
this.iplayingInformation = iplayingInformation;
return this;
}
public JoliviaBuilder clientSessionListener(final IClientSessionListener clientSessionListener) {
this.clientSessionListener = clientSessionListener;
return this;
}
public Jolivia build() throws Exception {
return new Jolivia(this);
}
class DefaultClientSessionListener implements IClientSessionListener {
private Session session;
public DefaultClientSessionListener() {
}
@Override
public void registerNewSession(final Session session) throws Exception {
this.session = session;
}
@Override
public void tearDownSession(final String server, final int port) {
try {
session.logout();
} catch (final Exception e) {
e.printStackTrace();
}
}
}
class DefaultIPlayingInformation implements IPlayingInformation {
private final JFrame frame;
private final JLabel label;
public DefaultIPlayingInformation() {
frame = new JFrame("Cover");
label = new JLabel();
frame.getContentPane().add(label, BorderLayout.CENTER);
frame.pack();
frame.setVisible(false);
}
@Override
public void notify(final BufferedImage image) {
try {
final ImageIcon icon = new ImageIcon(image);
label.setIcon(icon);
frame.pack();
frame.setSize(icon.getIconWidth(), icon.getIconHeight());
frame.setVisible(true);
} catch (final Exception e) {
LOGGER.debug(e.getMessage(), e);
}
}
@Override
public void notify(final ListingItem listingItem) {
final String title = listingItem.getSpecificChunk(ItemName.class).getValue();
final String artist = listingItem.getSpecificChunk(SongArtist.class).getValue();
final String album = listingItem.getSpecificChunk(SongAlbum.class).getValue();
frame.setTitle("Playing: " + title + " - " + album + " - " + artist);
}
}
}
private final JoliviaServer joliviaServer;
private final Server server;
private Jolivia(final JoliviaBuilder builder) throws Exception {
buildUI();
Preconditions.checkArgument(!(builder.pairingCode > 9999 || builder.pairingCode < 0),
"Pairingcode must be expressed within 4 ciphers");
LOGGER.info("Starting " + builder.name + " on port " + builder.port);
server = new Server(builder.port);
final ServerConnector dmapConnector = new ServerConnector(server, new DmapConnectionFactory());
dmapConnector.setPort(builder.port);
server.setConnectors(new Connector[] { dmapConnector });
// Guice
final ServletContextHandler sch = new ServletContextHandler(server, "/");
joliviaServer = new JoliviaServer(builder.port, builder.airplayPort, builder.pairingCode, builder.name,
builder.clientSessionListener, builder.speakerListener, builder.imageStoreReader,
builder.musicStoreReader, builder.iplayingInformation, builder.security, builder.appleUsername,
builder.applePassword);
sch.addEventListener(joliviaServer);
sch.addFilter(GuiceFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
final String username;
if (builder.security == PasswordMethod.PASSWORD)
username = DmapUtil.DEFAULT_USER;
else
username = "user";
final String password = "password";
if (builder.scheme == SecurityScheme.BASIC)
sch.setSecurityHandler(
getSecurityHandler(username, password, DmapUtil.DAAP_REALM, new BasicAuthenticator()));
if (builder.scheme == SecurityScheme.DIGEST)
sch.setSecurityHandler(
getSecurityHandler(username, password, DmapUtil.DAAP_REALM, new DigestAuthenticator()));
sch.addServlet(DefaultServlet.class, "/");
LOGGER.info(builder.name + " started");
}
public void start() throws Exception {
server.start();
}
public void stop() throws Exception {
joliviaServer.contextDestroyed(null);
server.stop();
}
public void reRegister() {
this.joliviaServer.reRegister();
}
private SecurityHandler getSecurityHandler(final String username, final String password, final String realm,
final Authenticator authenticator) {
final HashLoginService loginService = new HashLoginService();
loginService.putUser(username, Credential.getCredential(password), new String[] { "user" });
loginService.setName(realm);
final Constraint globalConstraint = new Constraint();
if (BasicAuthenticator.class.equals(authenticator.getClass()))
globalConstraint.setName(Constraint.__BASIC_AUTH);
if (DigestAuthenticator.class.equals(authenticator.getClass()))
globalConstraint.setName(Constraint.__DIGEST_AUTH);
globalConstraint.setRoles(new String[] { "user" });
globalConstraint.setAuthenticate(true);
final ConstraintMapping globalConstraintMapping = new ConstraintMapping();
globalConstraintMapping.setConstraint(globalConstraint);
globalConstraintMapping.setPathSpec("/*");
final ConstraintSecurityHandler csh = new ConstraintSecurityHandler();
csh.setAuthenticator(authenticator);
csh.setRealmName(realm);
csh.addConstraintMapping(globalConstraintMapping);
csh.addConstraintMapping(createRelaxation("/server-info"));
csh.addConstraintMapping(createRelaxation("/logout"));
// Following is a hack! It should state /databases/*/items/* instead -
// however, that cannot be used.
csh.addConstraintMapping(createRelaxation("/databases/*"));
csh.setLoginService(loginService);
return csh;
}
private static ConstraintMapping createRelaxation(final String pathSpec) {
final Constraint relaxation = new Constraint();
relaxation.setName(Constraint.ANY_ROLE);
relaxation.setAuthenticate(false);
final ConstraintMapping constraintMapping = new ConstraintMapping();
constraintMapping.setConstraint(relaxation);
constraintMapping.setPathSpec(pathSpec);
return constraintMapping;
}
private void buildUI() throws AWTException {
/* Create about dialog */
final Dialog aboutDialog = new Dialog((Dialog) null);
final GridBagLayout aboutLayout = new GridBagLayout();
aboutDialog.setLayout(aboutLayout);
aboutDialog.setVisible(false);
aboutDialog.setTitle("About Jolivia");
aboutDialog.setResizable(false);
{
/* Message */
final TextArea title = new TextArea(AboutMessage.split("\n").length + 1, 64);
title.setText(AboutMessage);
title.setEditable(false);
final GridBagConstraints titleConstraints = new GridBagConstraints();
titleConstraints.gridx = 1;
titleConstraints.gridy = 1;
titleConstraints.fill = GridBagConstraints.HORIZONTAL;
titleConstraints.insets = new Insets(0, 0, 0, 0);
aboutLayout.setConstraints(title, titleConstraints);
aboutDialog.add(title);
}
{
/* Done button */
final Button aboutDoneButton = new Button("Done");
aboutDoneButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(final ActionEvent evt) {
aboutDialog.setVisible(false);
}
});
final GridBagConstraints aboutDoneConstraints = new GridBagConstraints();
aboutDoneConstraints.gridx = 1;
aboutDoneConstraints.gridy = 2;
aboutDoneConstraints.anchor = GridBagConstraints.PAGE_END;
aboutDoneConstraints.fill = GridBagConstraints.NONE;
aboutDoneConstraints.insets = new Insets(0, 0, 0, 0);
aboutLayout.setConstraints(aboutDoneButton, aboutDoneConstraints);
aboutDialog.add(aboutDoneButton);
}
aboutDialog.setVisible(false);
aboutDialog.setLocationByPlatform(true);
aboutDialog.pack();
/* Create tray icon */
final URL trayIconUrl = Thread.currentThread().getContextClassLoader().getResource("Ceres.png");
final TrayIcon trayIcon1 = new TrayIcon(new ImageIcon(trayIconUrl, "AirReceiver").getImage());
trayIcon1.setToolTip("Jolivia");
trayIcon1.setImageAutoSize(true);
final PopupMenu popupMenu = new PopupMenu();
final MenuItem aboutMenuItem = new MenuItem("About");
aboutMenuItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(final ActionEvent evt) {
aboutDialog.setLocationByPlatform(true);
aboutDialog.setVisible(true);
}
});
popupMenu.add(aboutMenuItem);
final MenuItem exitMenuItem = new MenuItem("Quit");
exitMenuItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(final ActionEvent evt) {
try {
onShutdown();
} catch (final Exception e) {
LOGGER.info(e.getMessage(), e);
}
System.exit(0);
}
});
popupMenu.add(exitMenuItem);
trayIcon1.setPopupMenu(popupMenu);
SystemTray.getSystemTray().add(trayIcon1);
// trayIcon1.displayMessage("sldkjfsldkfj", "æslkfædslkf",
// MessageType.INFO);
if (!SystemTray.isSupported()) {
// Go directory to the task;
return;
}
}
protected void onShutdown() throws Exception {
stop();
}
}