/* * Copyright (c) 2010, Manuel Mausz. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * - Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * - Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * - The names of the authors may not be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ import static java.lang.System.err; import static java.lang.System.out; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.rmi.registry.*; import java.rmi.server.*; import java.rmi.*; import java.text.SimpleDateFormat; import java.lang.StringBuffer; import java.util.Collections; import java.util.Comparator; import java.util.Properties; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Arrays; import java.util.Date; import java.util.Set; import java.util.Map; import java.io.*; /* * Server implementation for Lab#2 of DSLab WS10 * See angabe.pdf for details * * This code is not documented at all. This is volitional * * @author Manuel Mausz (0728348) */ public class Server implements Serializable { public class ServerRecords extends HashMap {} /*==========================================================================*/ public class UserRecord implements Serializable { public final String name; public final String server; public String pass = null; public boolean committed = false; public S2CInterface s2ciface = null; UserRecord(String name, String server) { this.name = name; this.server = server; } /*------------------------------------------------------------------------*/ public synchronized boolean isCommitted() { return committed; } /*------------------------------------------------------------------------*/ public synchronized void commit() { committed = true; } } /*==========================================================================*/ public class UserRecords extends HashMap { public synchronized boolean register(String user, String pass) { if (!registerPrepare(user, bindingName)) return false; synchronized(servers) { ArrayList rollbacklist = new ArrayList(); boolean ret = true; for(String server : servers.keySet()) { try { ret = servers.get(server).registerPrepare(user, bindingName); } catch(RemoteException e) { ret = false; } if (!ret) break; rollbacklist.add(server); } if (!ret) { for(String server : rollbacklist) { try { servers.get(server).registerRollback(user); } catch(RemoteException e) {} } registerRollback(user); return false; } for(String server : servers.keySet()) { try { servers.get(server).registerCommit(user); } catch(RemoteException e) {} } registerCommit(user); get(user).pass = pass; } return true; } /*------------------------------------------------------------------------*/ public synchronized boolean registerPrepare(String user, String server) { if (containsKey(user)) return false; put(user, new UserRecord(user, server)); return true; } /*------------------------------------------------------------------------*/ public synchronized boolean registerCommit(String user) { if (!containsKey(user)) return false; get(user).commit(); return true; } /*------------------------------------------------------------------------*/ public synchronized boolean registerRollback(String user) { out.println("rollback"); if (!containsKey(user)) return true; if (get(user).isCommitted()) return false; remove(user); return true; } /*------------------------------------------------------------------------*/ public synchronized UserRecord login(String user, String pass, S2CInterface s2ciface) { UserRecord record = get(user); if (record == null) return null; synchronized(record) { if (record.pass == null || !record.pass.equals(pass) || !record.isCommitted()) return null; try { if (record.s2ciface != null) { record.s2ciface.notify("Another user logged in using your credentials. You will get logged out now."); record.s2ciface.logout(); } } catch(RemoteException e) {} record.s2ciface = s2ciface; } return record; } /*------------------------------------------------------------------------*/ public synchronized void logout(UserRecord user) { if (user != null) { synchronized(user) { user.s2ciface = null; } } } /*------------------------------------------------------------------------*/ public synchronized void notify(String user, String msg) throws RemoteException { UserRecord record = get(user); if (record == null) throw new RemoteException("User doesn't exist."); synchronized(record) { if (!record.isCommitted()) throw new RemoteException("User doesn't exist."); if (!record.server.equals(bindingName)) { S2SInterface tmp = servers.get(record.server); if (tmp != null) { try { tmp.notifyUser(user, msg); } catch(RemoteException e) {} } return; } if (record.s2ciface != null) record.s2ciface.notify(msg); } } } /*==========================================================================*/ public class EventRecord implements Serializable { public final String name; public String location; public int duration; public String author = null; public final String server; public boolean committed = false; public HashMap dates; public Date date = null; public HashSet invitees; public HashSet voted; EventRecord(String name, String server) { this.name = name; this.server = server; dates = new HashMap(); invitees = new HashSet(); voted = new HashSet(); } /*------------------------------------------------------------------------*/ public synchronized boolean isCommitted() { return committed; } /*------------------------------------------------------------------------*/ public synchronized void commit() { committed = true; } } /*==========================================================================*/ public class EventRecords extends HashMap { private SimpleDateFormat datefmt = new SimpleDateFormat("dd.MM.yyyy/HH:mm"); public synchronized boolean create(UserRecord user, String event, String location, int duration) { if (!createPrepare(event, bindingName)) return false; synchronized(servers) { ArrayList rollbacklist = new ArrayList(); boolean ret = true; for(String server : servers.keySet()) { try { ret = servers.get(server).createPrepare(event, bindingName); } catch(RemoteException e) { ret = false; } if (!ret) break; rollbacklist.add(server); } if (!ret) { for(String server : rollbacklist) { try { servers.get(server).createRollback(event); } catch(RemoteException e) {} } createRollback(event); return false; } for(String server : servers.keySet()) { try { servers.get(server).createCommit(event); } catch(RemoteException e) {} } createCommit(event); EventRecord record = get(event); synchronized(record) { record.location = location; record.duration = duration; synchronized(user) { record.author = user.name; } } } return true; } /*------------------------------------------------------------------------*/ public synchronized boolean createPrepare(String event, String server) { if (containsKey(event)) return false; put(event, new EventRecord(event, server)); return true; } /*------------------------------------------------------------------------*/ public synchronized boolean createCommit(String event) { if (!containsKey(event)) return false; get(event).commit(); return true; } /*------------------------------------------------------------------------*/ public synchronized boolean createRollback(String event) { if (!containsKey(event)) return true; if (get(event).isCommitted()) return false; remove(event); return true; } /*------------------------------------------------------------------------*/ public synchronized void addDate(UserRecord user, String event, Date date) throws RemoteException { EventRecord record = get(event); if (record == null) throw new RemoteException("Event doesn't exist."); synchronized(record) { if (!record.isCommitted()) throw new RemoteException("Event doesn't exist."); synchronized(user) { if (record.author == null || !record.author.equals(user.name)) throw new RemoteException("You're not the author of that event."); } if (record.date != null) throw new RemoteException("Event already have been finalized."); if (record.dates.containsKey(date)) throw new RemoteException("Date already exists."); record.dates.put(date, 0); } } /*------------------------------------------------------------------------*/ public synchronized void invite(UserRecord user, String event, String invitee) throws RemoteException { EventRecord record = get(event); if (record == null) throw new RemoteException("Event doesn't exist."); synchronized(record) { if (!record.isCommitted()) throw new RemoteException("Event doesn't exist."); synchronized(user) { if (record.author == null || !record.author.equals(user.name)) throw new RemoteException("You're not the author of that event."); } if (record.author.equals(invitee)) throw new RemoteException("You can't invite yourself."); if (record.date != null) throw new RemoteException("Event already have been finalized."); if (record.invitees.contains(invitee)) throw new RemoteException("User already invited."); users.notify(invitee, "You have been invited to event \"" + event + "\""); record.invitees.add(invitee); } } /*------------------------------------------------------------------------*/ public synchronized String toString(String event) throws RemoteException { EventRecord record = get(event); if (record == null) throw new RemoteException("Event doesn't exist."); synchronized(record) { if (!record.isCommitted()) throw new RemoteException("Event doesn't exist."); if (!record.server.equals(bindingName)) { S2SInterface tmp = servers.get(record.server); if (tmp == null) throw new RemoteException("Internal Error: Remote server doesn't exist anymore."); return tmp.eventToString(event); } StringBuffer sb = new StringBuffer(100); sb.append("Event: ").append(record.name).append("\n"); sb.append("Location: ").append(record.location).append("\n"); sb.append("Duration: ").append(record.duration).append(" min.\n"); sb.append("Author: ").append(record.author).append("\n"); if (record.date != null) sb.append("Date: ").append(datefmt.format(record.date)).append("\n"); else { if (record.dates.size() == 0) sb.append("Options: None set\n"); else { sb.append("Options:\n"); for(Date date : record.dates.keySet()) sb.append(" * ").append(datefmt.format(date)).append(" ... ") .append(record.dates.get(date)).append(" votes\n"); } } if (record.invitees.size() == 0) sb.append("Invitees: None set\n"); else sb.append("Invitees: ").append(Utils.join(record.invitees, ", ")).append("\n"); return sb.toString(); } } /*------------------------------------------------------------------------*/ public synchronized void vote(String event, String user, Date[] dates) throws RemoteException { EventRecord record = get(event); if (record == null) throw new RemoteException("Event doesn't exist."); synchronized(record) { if (!record.isCommitted()) throw new RemoteException("Event doesn't exist."); if (!record.server.equals(bindingName)) { S2SInterface tmp = servers.get(record.server); if (tmp == null) throw new RemoteException("Internal Error: Remote server doesn't exist anymore."); try { tmp.vote(event, user, dates); } catch(RemoteException e) { throw new RemoteException(e.getCause().getMessage()); } return; } if (record.date != null) throw new RemoteException("Event already have been finalized."); if (record.voted.contains(user)) throw new RemoteException("You have already voted."); if ((record.author == null || !record.author.equals(user)) && !record.invitees.contains(user)) throw new RemoteException("You're not allowed to vote for that event."); Set recorddates = record.dates.keySet(); for(Date date : dates) { if (!recorddates.contains(date)) throw new RemoteException("Date \"" + datefmt.format(date) + "\" is not a valid option."); } for(Date date : dates) record.dates.put(date, record.dates.get(date) + 1); record.voted.add(user); } } /*------------------------------------------------------------------------*/ public synchronized void finalize(UserRecord user, String event) throws RemoteException { EventRecord record = get(event); if (record == null) throw new RemoteException("Event doesn't exist."); synchronized(record) { synchronized(user) { if (!record.isCommitted()) throw new RemoteException("Event doesn't exist."); if (record.author == null || !record.author.equals(user.name)) throw new RemoteException("You're not the author of that event."); if (record.date != null) throw new RemoteException("Event has already been finalized."); record.date = Collections.max(record.dates.entrySet(), new Comparator>() { public int compare(Map.Entry o1, Map.Entry o2) { return o1.getValue().compareTo(o2.getValue()); } }).getKey(); String text = "Event \"" + event + "\" has been scheduled on " + datefmt.format(record.date) + "."; users.notify(user.name, text); for(String invitee : record.invitees) users.notify(invitee, text); } } } } /*==========================================================================*/ public class WaitForServers implements Runnable { private ArrayList serverlist; WaitForServers() { serverlist = new ArrayList(); serverlist.addAll(Arrays.asList(serverNames)); serverlist.add(bindingName); Collections.sort(serverlist); } /*------------------------------------------------------------------------*/ public void shutdown() { scheduler.shutdown(); synchronized(mainLock) { mainLock.notify(); } } /*------------------------------------------------------------------------*/ public void run() { synchronized(registry) { try { String[] tmp = registry.list(); Collections.sort(Arrays.asList(tmp)); if (serverlist.equals(Arrays.asList(tmp))) { synchronized(servers) { for(String server : tmp) { if (server.equals(bindingName)) continue; servers.put(server, ((RemoteInterface)registry.lookup(server)).s2sInterfaceFactory()); } } shutdown(); } } catch(NotBoundException e) { err.println("Error: " + e.getMessage()); /* this shouldn't happen */ shutdown(); } catch(RemoteException e) { err.println("Unable to lookup servers: " + e.getMessage()); shutdown(); } } } } /*==========================================================================*/ public class Interactive extends CommandInteractive implements Runnable { private final InputStream sin; private final Object mainLock; Interactive(InputStream sin, Object mainLock) throws NoSuchMethodException, IOException { this.sin = sin; this.mainLock = mainLock; setIgnoreEmptyMode(false); cmdHandler.register("unknown", this, "cmdUnknown"); } /*------------------------------------------------------------------------*/ public void cmdUnknown(String cmd, String[] args) throws IOException { stop(); } /*------------------------------------------------------------------------*/ public void shutdown() {} /*------------------------------------------------------------------------*/ public void run() { try { run(sin); } catch(CommandHandler.Exception e) { err.println("Internal Error: " + e.getMessage()); } catch(IOException e) { /* ignore that exception * thread will shutdown and unlock the main thread * which will shutdown the application */ } shutdown(); synchronized(mainLock) { mainLock.notify(); } } } /*==========================================================================*/ private static String bindingName; private static boolean initRegistry; private static String[] serverNames; private static InputStream stdin = null; private static Thread tInteractive = null; private static ScheduledExecutorService scheduler = null; private static Registry registry = null; private static RemoteInterface riface = null; private static ServerRecords servers; public UserRecords users; public EventRecords events; public volatile boolean ready = false; private static final Object mainLock = new Object(); /*--------------------------------------------------------------------------*/ Server() { servers = new ServerRecords(); users = new UserRecords(); events = new EventRecords(); } /*--------------------------------------------------------------------------*/ public static void usage() throws Utils.Shutdown { out.println("Usage: Server bindingName initRegistry serverNames\n"); out.println("bindingName\t...the name this server shall use to bind its remote"); out.println("\t\t reference in the RMI registry"); out.println("initRegistry\t...a boolean value, i.e. either true or false,"); out.println("\t\t indicating whether this server is responsible for"); out.println("\t\t creating the RMI registry or not"); out.println("serverNames\t...a list of names, separated by space characters,"); out.println("\t\t indicating the name of the other servers' remote references"); // Java is some piece of crap which doesn't allow me to set exitcode w/o // using System.exit. Maybe someday Java will be a fully functional // programming language, but I wouldn't bet my money //System.exit(1); throw new Utils.Shutdown("FUCK YOU JAVA"); } /*--------------------------------------------------------------------------*/ public void bailout(String error) throws Utils.Shutdown { if (error != null) err.println("Error: " + error); shutdown(); // Java is some piece of crap which doesn't allow me to set exitcode w/o // using System.exit. Maybe someday Java will be a fully functional // programming language, but I wouldn't bet my money //System.exit(2); throw new Utils.Shutdown("FUCK YOU JAVA"); } /*--------------------------------------------------------------------------*/ public void parseArgs(String[] args) { if (args.length != 3) usage(); bindingName = args[0]; if (bindingName.length() == 0) bailout("bindingName is empty"); initRegistry = false; if (args[1].equals("true")) initRegistry = true; else if (args[1].equals("false")) initRegistry = false; else bailout("initRegistry must be either \"true\" or \"false\""); serverNames = args[2].split(" "); if (Arrays.asList(serverNames).contains(bindingName)) bailout("bindingName found in serverNames. You shouldn't do that!"); } /*--------------------------------------------------------------------------*/ public void createInterface() { String regHost = null; int regPort = 0; try { InputStream in = ClassLoader.getSystemResourceAsStream("registry.properties"); if (in == null) bailout("Properties file doesn't exist or isn't readable"); Properties props = new Properties(); props.load(in); for(String prop : props.stringPropertyNames()) { if (prop.equals("registry.host")) regHost = props.getProperty(prop); else if (prop.equals("registry.port")) { try { regPort = Integer.parseInt(props.getProperty(prop)); if (regPort <= 0 || regPort > 65536) { err.println("Property " + prop + " must be a valid port number (1 - 65535). Skipping..."); regPort = 0; } } catch(NumberFormatException e) { err.println("Property " + prop + " must be numeric. Skipping..."); } } else err.println("Property " + prop + " is unknown. Skipping..."); } in.close(); } catch(IOException e) { bailout("Unable to read from properties file: " + e.getMessage()); } if (!initRegistry && (regHost == null || regHost.length() <= 0)) bailout("Registry host is not set"); if (regPort == 0) bailout("Registry port is not set"); try { if (initRegistry) registry = LocateRegistry.createRegistry(regPort); else registry = LocateRegistry.getRegistry(regHost, regPort); riface = new RemoteInterfaceImpl(this); //DEBUG RemoteServer.setLog(System.out); registry.bind(bindingName, riface); } catch(RemoteException e) { bailout("Unable to get/create registry: " + e.getMessage()); } catch(AlreadyBoundException e) { registry = null; bailout("Unable to bind remote interface. Already bound: " + e.getMessage()); } out.println("Remote interface successfully exported!"); } /*--------------------------------------------------------------------------*/ public void writeData(String file) { if (file == null) return; try { ObjectOutputStream sout = new ObjectOutputStream(new FileOutputStream(file)); sout.writeObject(users); sout.writeObject(events); sout.close(); } catch(FileNotFoundException e) {} catch(IOException e) {} } /*--------------------------------------------------------------------------*/ public void readData(String file) { if (file == null) return; try { ObjectInputStream sin = new ObjectInputStream(new FileInputStream(file)); users = (UserRecords)sin.readObject(); events = (EventRecords)sin.readObject(); sin.close(); } catch(ClassNotFoundException e) {} catch(FileNotFoundException e) {} catch(IOException e) {} } /*--------------------------------------------------------------------------*/ public void shutdown() { for(String user : users.keySet()) { UserRecord record = users.get(user); synchronized(record) { record.s2ciface = null; } } try { if (scheduler != null) { scheduler.shutdownNow(); scheduler.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); } } catch(InterruptedException e) {} try { if (riface != null) riface.unexportAll(); } catch(RemoteException e) {} try { if (registry != null) registry.unbind(bindingName); } catch(NotBoundException e) {} catch(RemoteException e) {} try { if (tInteractive != null) { tInteractive.interrupt(); tInteractive.join(); } } catch(InterruptedException e) {} try { if (stdin != null) stdin.close(); } catch(IOException e) {} //PERSISTENT writeData(bindingName + ".data"); } /*--------------------------------------------------------------------------*/ public void run(String[] args) { boolean shutdown = false; parseArgs(args); //PERSISTENT readData(bindingName + ".data"); synchronized(mainLock) { createInterface(); try { InputStream stdin = java.nio.channels.Channels.newInputStream( new FileInputStream(FileDescriptor.in).getChannel()); tInteractive = new Thread(new Interactive(stdin, mainLock)); tInteractive.start(); } catch(NoSuchMethodException e) { bailout("Unable to setup interactive command handler"); } catch(IOException e) { bailout("Unable to create object output stream: " + e.getMessage()); } try { out.println("Waiting for the other servers to come online..."); scheduler = Executors.newScheduledThreadPool(1); ScheduledFuture waitServerTimer = scheduler.scheduleAtFixedRate( new WaitForServers(), 0, 500, TimeUnit.MILLISECONDS); mainLock.wait(); synchronized(servers) { if (!scheduler.isShutdown() || servers.size() != serverNames.length) shutdown = true; } } catch(InterruptedException e) { /* if we get interrupted -> ignore */ } if (!shutdown) { ready = true; out.println("Server startup successful!"); try { mainLock.wait(); } catch(InterruptedException e) { /* if we get interrupted -> ignore */ } } } out.println("Shutting down..."); shutdown(); } /*--------------------------------------------------------------------------*/ public static void main(String[] args) { try { Server srv = new Server(); srv.run(args); } catch(Utils.Shutdown e) {} } }