/* ********************************************************************** ** ** Copyright notice ** ** ** ** (c) 2005-2009 RSSOwl Development Team ** ** http://www.rssowl.org/ ** ** ** ** All rights reserved ** ** ** ** This program and the accompanying materials are made available under ** ** the terms of the Eclipse Public License v1.0 which accompanies this ** ** distribution, and is available at: ** ** http://www.rssowl.org/legal/epl-v10.html ** ** ** ** A copy is found in the file epl-v10.html and important notices to the ** ** license from the team is found in the textfile LICENSE.txt distributed ** ** in this package. ** ** ** ** This copyright notice MUST APPEAR in all copies of the file! ** ** ** ** Contributors: ** ** RSSOwl Development Team - initial API and implementation ** ** ** ** ********************************************************************** */ package org.rssowl.ui.internal; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.SafeRunner; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.jface.viewers.ContentViewer; import org.eclipse.jface.viewers.IStructuredContentProvider; import org.rssowl.core.persist.IBookMark; import org.rssowl.core.persist.IEntity; import org.rssowl.core.persist.INews; import org.rssowl.core.persist.INewsBin; import org.rssowl.core.persist.ISearchMark; import org.rssowl.core.persist.reference.BookMarkReference; import org.rssowl.core.persist.reference.FolderReference; import org.rssowl.core.persist.reference.ModelReference; import org.rssowl.core.persist.reference.NewsBinReference; import org.rssowl.core.persist.reference.NewsReference; import org.rssowl.core.persist.reference.SearchMarkReference; import org.rssowl.core.util.CoreUtils; import org.rssowl.core.util.LoggingSafeRunnable; import org.rssowl.core.util.StringUtils; import org.rssowl.core.util.URIUtils; import org.rssowl.ui.internal.FolderNewsMark.FolderNewsMarkReference; import org.rssowl.ui.internal.editors.feed.NewsBrowserLabelProvider; import org.rssowl.ui.internal.editors.feed.NewsBrowserViewer; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.net.BindException; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.UnknownHostException; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.StringTokenizer; import java.util.concurrent.ConcurrentHashMap; /** * The <code>NewsServer</code> is a Singleton that serves HTML for a request of * News. A Browser can navigate to a local URL with Port 8795 and use some * special parameters to request either a List of News or complete Feeds. * <p> * TODO As more and more stuff is handled by this server, it should be * considered to make it extensible by allowing to register handlers for certain * operations. * </p> * * @author bpasero */ public class ApplicationServer { /* The Singleton Instance */ private static ApplicationServer fgInstance = new ApplicationServer(); /* Local URL Default Values */ static final String PROTOCOL = "http"; //$NON-NLS-1$ public static final String DEFAULT_LOCALHOST = "127.0.0.1"; //$NON-NLS-1$ @SuppressWarnings("all") static final int DEFAULT_SOCKET_PORT = Application.IS_ECLIPSE ? 8775 : 8795; /* Local URL Parts */ static String LOCALHOST = DEFAULT_LOCALHOST; static int SOCKET_PORT = DEFAULT_SOCKET_PORT; /* Handshake Message */ static final String STARTUP_HANDSHAKE = "org.rssowl.ui.internal.StartupHandshake"; //$NON-NLS-1$ /* DWord controlling the startup-handshake */ private static final String MULTI_INSTANCE_PROPERTY = "multiInstance"; //$NON-NLS-1$ /* DWord controlling the localhost value */ private static final String LOCALHOST_PROPERTY = "localhost"; //$NON-NLS-1$ /* DWord controlling the port value */ private static final String PORT_PROPERTY = "port"; //$NON-NLS-1$ /* Identifies the Viewer providing the Content */ private static final String ID = "id="; //$NON-NLS-1$ /* Used after all HTTP-Headers */ private static final String CRLF = "\r\n"; //$NON-NLS-1$ /* Registry of known Viewer */ private static Map<String, ContentViewer> fRegistry = new ConcurrentHashMap<String, ContentViewer>(); /* Supported Operations */ private static final String OP_DISPLAY_FOLDER = "displayFolder="; //$NON-NLS-1$ private static final String OP_DISPLAY_BOOKMARK = "displayBookMark="; //$NON-NLS-1$ private static final String OP_DISPLAY_NEWSBIN = "displayNewsBin="; //$NON-NLS-1$ private static final String OP_DISPLAY_SEARCHMARK = "displaySearchMark="; //$NON-NLS-1$ private static final String OP_DISPLAY_NEWS = "displayNews="; //$NON-NLS-1$ private static final String OP_RESOURCE = "resource="; //$NON-NLS-1$ /* Windows only: Mark of the Web */ private static final String IE_MOTW = "<!-- saved from url=(0014)about:internet -->"; //$NON-NLS-1$ /* RFC 1123 Date Format for the respond header */ private static final DateFormat RFC_1123_DATE = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.ENGLISH); //$NON-NLS-1$ /* Interface used to handle a startup-handshake */ static interface HandshakeHandler { /** * Handler for the hand-shake done on startup in case the application server * was found already running by another RSSOwl process. * * @param token A message to pass via hand-shake. */ void handle(String token); } private ServerSocket fSocket; private Job fServerJob; private int fPort; private HandshakeHandler fHandshakeHandler; /** * Returns the singleton instance of the ApplicationServer. * * @return the singleton instance of the ApplicationServer. */ public static ApplicationServer getDefault() { return fgInstance; } /** * Attempts to start the server. Will throw an IOException in case of a * problem. * * @throws IOException in case of a problem starting the server. * @throws UnknownHostException in case the host is unknown. * @throws BindException in case of a failure binding the server to a port. */ public void startup() throws IOException { /* Server already running */ if (isRunning()) return; /* Determine Localhost Value */ String localhostProperty = System.getProperty(LOCALHOST_PROPERTY); if (localhostProperty != null && localhostProperty.length() > 0) LOCALHOST = localhostProperty; /* Determine Port Value */ String portProperty = System.getProperty(PORT_PROPERTY); if (portProperty != null && portProperty.length() > 0) { try { SOCKET_PORT = Integer.parseInt(portProperty); } catch (NumberFormatException e) { Activator.getDefault().logError(e.getMessage(), e); } } /* Server not yet running */ boolean usePortRange = Application.IS_ECLIPSE || System.getProperty(MULTI_INSTANCE_PROPERTY) != null; fSocket = createServerSocket(usePortRange); if (fSocket != null) listen(); } /** Stop the Application Server */ public void shutdown() { fServerJob.cancel(); try { if (fSocket != null) fSocket.close(); } catch (IOException e) { if (Activator.getDefault() != null) Activator.getDefault().logError(e.getMessage(), e); } } /** * Check if the server is running or not. * * @return <code>TRUE</code> in case the server is running and * <code>FALSE</code> otherwise. */ public boolean isRunning() { return fSocket != null; } /* Registers the Handler for Hand-Shaking on startup */ void setHandshakeHandler(HandshakeHandler handler) { fHandshakeHandler = handler; } /* Attempt to create Server-Socket with retry-option */ private ServerSocket createServerSocket(boolean usePortRange) throws IOException { /* Ports to try */ List<Integer> ports = new ArrayList<Integer>(); ports.add(SOCKET_PORT); /* Try up to 10 different ports if set */ if (usePortRange) { for (int i = 1; i < 10; i++) ports.add(SOCKET_PORT + i); } /* Attempt to open Port */ for (int i = 0; i < ports.size(); i++) { try { int port = ports.get(i); fPort = port; return new ServerSocket(fPort, 50, InetAddress.getByName(LOCALHOST)); } catch (UnknownHostException e) { throw e; } catch (BindException e) { if (i == (ports.size() - 1)) throw e; } } return null; } /** * Returns <code>TRUE</code> if the given URL is a local NewsServer URL. * * @param url The URL to check for being a NewsServer URL. * @return <code>TRUE</code> if the given URL is a local NewsServer URL. */ public boolean isNewsServerUrl(String url) { return url.contains(LOCALHOST) && url.contains(String.valueOf(fPort)); } /** * Registers a Viewer under a certain ID to the Registry. Viewers need to * register if they want to use the Server. Based on the ID, the Server is * asking the correct Viewer for the Content. * * @param id The unique ID under which the Viewer is stored in the registry. * @param viewer The Viewer to store in the registry. */ public void register(String id, ContentViewer viewer) { fRegistry.put(id, viewer); } /** * Removes a Viewer from the registry. * * @param id The ID of the Viewer to remove from the registry. */ public void unregister(String id) { fRegistry.remove(id); } /** * Check wether the given URL contains one of the display-operations of this * Server. * * @param url The URL to Test for a Display Operation. * @return Returns <code>TRUE</code> if the given URL is a display-operation. */ public boolean isDisplayOperation(String url) { if (!StringUtils.isSet(url)) return false; return url.contains(OP_DISPLAY_FOLDER) || url.contains(OP_DISPLAY_BOOKMARK) || url.contains(OP_DISPLAY_NEWSBIN) || url.contains(OP_DISPLAY_NEWS) || url.contains(OP_DISPLAY_SEARCHMARK) || URIUtils.ABOUT_BLANK.equals(url); } /** * Check wether the given URL contains one of the resource-operations of this * Server. * * @param url The URL to Test for a Resource Operation. * @return Returns <code>TRUE</code> if the given URL is a resource-operation. */ public boolean isResourceOperation(String url) { if (!StringUtils.isSet(url)) return false; return url.contains(OP_RESOURCE); } /** * @param path the path to the resource in the plugin. * @return a url that can be used to access the resource. */ public String toResourceUrl(String path) { StringBuilder url = new StringBuilder(); url.append(PROTOCOL).append("://").append(LOCALHOST).append(':').append(fPort).append("/"); //$NON-NLS-1$ //$NON-NLS-2$ url.append("?").append(OP_RESOURCE).append(path); //$NON-NLS-1$ return url.toString(); } /** * Creates a valid URL for the given Input * * @param id The ID of the Viewer * @param input The Input of the Viewer * @return a valid URL for the given Input */ public String toUrl(String id, Object input) { /* Handle this Case */ if (input == null) return URIUtils.ABOUT_BLANK; StringBuilder url = new StringBuilder(); url.append(PROTOCOL).append("://").append(LOCALHOST).append(':').append(fPort).append("/"); //$NON-NLS-1$ //$NON-NLS-2$ /* Append the ID */ url.append("?").append(ID).append(id); //$NON-NLS-1$ /* Wrap into Object Array */ if (!(input instanceof Object[])) input = new Object[] { input }; /* Input is an Array of Objects */ List<Long> news = new ArrayList<Long>(); List<Long> bookmarks = new ArrayList<Long>(); List<Long> newsbins = new ArrayList<Long>(); List<Long> searchmarks = new ArrayList<Long>(); List<Long> folders = new ArrayList<Long>(); /* Split into BookMarks, NewsBins, SearchMarks and News */ for (Object obj : (Object[]) input) { if (obj instanceof FolderNewsMarkReference) folders.add(getId(obj)); else if (obj instanceof IBookMark || obj instanceof BookMarkReference) bookmarks.add(getId(obj)); else if (obj instanceof INewsBin || obj instanceof NewsBinReference) newsbins.add(getId(obj)); else if (obj instanceof ISearchMark || obj instanceof SearchMarkReference) searchmarks.add(getId(obj)); else if (obj instanceof INews || obj instanceof NewsReference) news.add(getId(obj)); else if (obj instanceof EntityGroup) { List<EntityGroupItem> items = ((EntityGroup) obj).getItems(); for (EntityGroupItem item : items) { IEntity entity = item.getEntity(); if (entity instanceof INews) news.add(getId(entity)); } } } /* Append Parameter for Folders */ appendParameters(url, folders, OP_DISPLAY_FOLDER); /* Append Parameter for Bookmarks */ appendParameters(url, bookmarks, OP_DISPLAY_BOOKMARK); /* Append Parameter for Newsbins */ appendParameters(url, newsbins, OP_DISPLAY_NEWSBIN); /* Append Parameter for SearchMarks */ appendParameters(url, searchmarks, OP_DISPLAY_SEARCHMARK); /* Append Parameter for News */ appendParameters(url, news, OP_DISPLAY_NEWS); return url.toString(); } private void appendParameters(StringBuilder url, List<Long> ids, String op) { if (!ids.isEmpty()) { url.append("&").append(op); //$NON-NLS-1$ for (Long id : ids) url.append(id).append(','); /* Remove the last added ',' */ url.deleteCharAt(url.length() - 1); } } private Long getId(Object obj) { if (obj instanceof IEntity) return ((IEntity) obj).getId(); else if (obj instanceof ModelReference) return ((ModelReference) obj).getId(); return null; } private void listen() { /* Create a Job to listen for Requests */ fServerJob = new Job("Local News Viewer Server") { //$NON-NLS-1$ @Override protected IStatus run(IProgressMonitor monitor) { /* Listen as long not canceled */ while (!monitor.isCanceled()) { BufferedReader buffReader = null; Socket socket = null; try { /* Blocks until Socket accepted */ socket = fSocket.accept(); /* Read Incoming Message */ buffReader = new BufferedReader(new InputStreamReader(socket.getInputStream())); String message = buffReader.readLine(); /* Process Message */ if (StringUtils.isSet(message)) safeProcess(socket, message); } catch (IOException e) { /* Ignore */ } /* Cleanup */ finally { /* Close the Reader */ try { if (buffReader != null) buffReader.close(); } catch (Exception e) { /* Ignore */ } /* Close the Socket */ try { if (socket != null) socket.close(); } catch (Exception e) { /* Ignore */ } } } return Status.OK_STATUS; } }; /* Set as System-Job and Schedule */ fServerJob.setSystem(true); fServerJob.schedule(); } /* Process Message in Safe-Runnable */ private void safeProcess(final Socket socket, final String message) { final boolean isDisplayOperation = isDisplayOperation(message); final boolean isResourceOperation = !isDisplayOperation && isResourceOperation(message); LoggingSafeRunnable runnable = new LoggingSafeRunnable() { public void run() throws Exception { /* This is a Display-Operation */ if (isDisplayOperation) processDisplayOperation(socket, message); /* This is a Resource-Operation */ else if (isResourceOperation) processResourceOperation(socket, message); /* This is a startup handshake */ else processHandshake(message); } }; /* * For some reason using SafeRunner from this method can cause in a Classloader deadlock * where Equinox will terminate classloading after 5000 ms to avoid it. This happens when * two instances of RSSOwl start at the same time, e.g. when clicking the icon twice. */ if (!isDisplayOperation && !isResourceOperation) { try { runnable.run(); } catch (Exception e) { runnable.handleException(e); } } else SafeRunner.run(runnable); } /* Process Handshake-Message */ private void processHandshake(String message) { if (fHandshakeHandler != null) fHandshakeHandler.handle(message); } private void processResourceOperation(Socket socket, String message) { /* Substring to get the Parameters String */ int start = message.indexOf(OP_RESOURCE) + OP_RESOURCE.length(); int end = message.indexOf(' ', start); String parameter = message.substring(start, end); /* Write HTML to the Receiver */ BufferedOutputStream outS = null; try { outS = new BufferedOutputStream(socket.getOutputStream()); CoreUtils.copy(OwlUI.class.getResourceAsStream(parameter), outS); } catch (IOException e) { /* Ignore */ } /* Cleanup */ finally { if (outS != null) { try { outS.close(); } catch (IOException e) { /* Ignore */ } } } } /* Process Message by looking for operations */ private void processDisplayOperation(Socket socket, String message) { List<Object> elements = new ArrayList<Object>(); /* Substring to get the Parameters String */ int start = message.indexOf('/'); int end = message.indexOf(' ', start); String parameters = message.substring(start, end); /* Retrieve the ID */ String viewerId = null; int idIndex = parameters.indexOf(ID); if (idIndex >= 0) { start = idIndex + ID.length(); end = parameters.indexOf('&', start); if (end < 0) end = parameters.length(); viewerId = parameters.substring(start, end); } /* Ask for ContentProvider of Viewer */ ContentViewer viewer = fRegistry.get(viewerId); if (viewer instanceof NewsBrowserViewer && viewer.getContentProvider() != null) { IStructuredContentProvider newsContentProvider = (IStructuredContentProvider) viewer.getContentProvider(); /* Look for Folders that are to displayed */ int displayFolderIndex = parameters.indexOf(OP_DISPLAY_FOLDER); if (displayFolderIndex >= 0) { start = displayFolderIndex + OP_DISPLAY_FOLDER.length(); end = parameters.indexOf('&', start); if (end < 0) end = parameters.length(); StringTokenizer tokenizer = new StringTokenizer(parameters.substring(start, end), ",");//$NON-NLS-1$ while (tokenizer.hasMoreElements()) { FolderReference ref = new FolderReference(Long.valueOf((String) tokenizer.nextElement())); elements.addAll(Arrays.asList(newsContentProvider.getElements(ref))); } } /* Look for BookMarks that are to displayed */ int displayBookMarkIndex = parameters.indexOf(OP_DISPLAY_BOOKMARK); if (displayBookMarkIndex >= 0) { start = displayBookMarkIndex + OP_DISPLAY_BOOKMARK.length(); end = parameters.indexOf('&', start); if (end < 0) end = parameters.length(); StringTokenizer tokenizer = new StringTokenizer(parameters.substring(start, end), ",");//$NON-NLS-1$ while (tokenizer.hasMoreElements()) { BookMarkReference ref = new BookMarkReference(Long.valueOf((String) tokenizer.nextElement())); elements.addAll(Arrays.asList(newsContentProvider.getElements(ref))); } } /* Look for NewsBins that are to displayed */ int displayNewsBinsIndex = parameters.indexOf(OP_DISPLAY_NEWSBIN); if (displayNewsBinsIndex >= 0) { start = displayNewsBinsIndex + OP_DISPLAY_NEWSBIN.length(); end = parameters.indexOf('&', start); if (end < 0) end = parameters.length(); StringTokenizer tokenizer = new StringTokenizer(parameters.substring(start, end), ",");//$NON-NLS-1$ while (tokenizer.hasMoreElements()) { NewsBinReference ref = new NewsBinReference(Long.valueOf((String) tokenizer.nextElement())); elements.addAll(Arrays.asList(newsContentProvider.getElements(ref))); } } /* Look for SearchMarks that are to displayed */ int displaySearchMarkIndex = parameters.indexOf(OP_DISPLAY_SEARCHMARK); if (displaySearchMarkIndex >= 0) { start = displaySearchMarkIndex + OP_DISPLAY_SEARCHMARK.length(); end = parameters.indexOf('&', start); if (end < 0) end = parameters.length(); StringTokenizer tokenizer = new StringTokenizer(parameters.substring(start, end), ",");//$NON-NLS-1$ while (tokenizer.hasMoreElements()) { SearchMarkReference ref = new SearchMarkReference(Long.valueOf((String) tokenizer.nextElement())); elements.addAll(Arrays.asList(newsContentProvider.getElements(ref))); } } /* Look for News that are to displayed */ int displayNewsIndex = parameters.indexOf(OP_DISPLAY_NEWS); if (displayNewsIndex >= 0) { start = displayNewsIndex + OP_DISPLAY_NEWS.length(); end = parameters.indexOf('&', start); if (end < 0) end = parameters.length(); StringTokenizer tokenizer = new StringTokenizer(parameters.substring(start, end), ",");//$NON-NLS-1$ while (tokenizer.hasMoreElements()) { NewsReference ref = new NewsReference(Long.valueOf((String) tokenizer.nextElement())); elements.addAll(Arrays.asList(newsContentProvider.getElements(ref))); } } } /* Reply to the Socket */ reply(socket, viewerId, elements.toArray()); } /* Create HTML out of the Elements and reply to the Socket */ private void reply(Socket socket, String viewerId, Object[] elements) { /* Only responsible for Viewer-Concerns */ if (viewerId == null) return; /* Retrieve Viewer */ ContentViewer viewer = fRegistry.get(viewerId); /* Might be bad timing */ if (viewer == null) return; /* Ask for sorted Elements */ NewsBrowserLabelProvider labelProvider = (NewsBrowserLabelProvider) viewer.getLabelProvider(); Object[] children = new Object[0]; if (viewer instanceof NewsBrowserViewer) { children = ((NewsBrowserViewer) viewer).getFlattendChildren(elements); ((NewsBrowserViewer) viewer).updateViewModel(children); } /* Write HTML to the Receiver */ BufferedWriter writer = null; try { boolean portable = Controller.getDefault().isPortable(); writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); if (Application.IS_WINDOWS && portable) writer.append("HTTP/1.1 205 OK").append(CRLF); //$NON-NLS-1$ else writer.append("HTTP/1.1 200 OK").append(CRLF); //$NON-NLS-1$ synchronized (RFC_1123_DATE) { writer.append("Date: ").append(RFC_1123_DATE.format(new Date())).append(CRLF); //$NON-NLS-1$ } writer.append("Server: RSSOwl Local Server").append(CRLF); //$NON-NLS-1$ writer.append("Content-Type: text/html; charset=UTF-8").append(CRLF); //$NON-NLS-1$ writer.append("Connection: close").append(CRLF); //$NON-NLS-1$ writer.append("Expires: 0").append(CRLF); //$NON-NLS-1$ writer.write(CRLF); /* Begin HTML */ writer.write("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">\n"); //$NON-NLS-1$ /* Windows only: Mark of the Web */ if (Application.IS_WINDOWS) { writer.write(IE_MOTW); writer.write("\n"); //$NON-NLS-1$ } writer.write("<html>\n <head>\n"); //$NON-NLS-1$ /* Append Base URI if available */ String base = getBase(children); if (base != null) { writer.write(" <base href=\""); //$NON-NLS-1$ writer.write(base); writer.write("\">"); //$NON-NLS-1$ } writer.write("\n <title></title>"); //$NON-NLS-1$ writer.write("\n <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n"); //$NON-NLS-1$ /* CSS */ labelProvider.writeCSS(writer); /* Open Body */ writer.write(" </head>\n <body id=\"owlbody\">\n"); //$NON-NLS-1$ /* Output each Element as HTML */ for (int i = 0; i < children.length; i++) { String html = unicodeToEntities(labelProvider.getText(children[i], true, true, i)); writer.write(html); } /* End HTML */ writer.write("\n </body>\n</html>"); //$NON-NLS-1$ } catch (IOException e) { /* Ignore */ } /* Cleanup */ finally { if (writer != null) { try { writer.close(); } catch (IOException e) { /* Ignore */ } } } } /* Find BASE-Information from Elements */ private String getBase(Object elements[]) { for (Object object : elements) { if (object instanceof INews) { INews news = (INews) object; /* Base-Information explicitly set */ if (news.getBase() != null) return URIUtils.toHTTP(news.getBase()).toString(); /* Use Feed's Link as fallback */ return URIUtils.toHTTP(news.getFeedLinkAsText()); } } return null; } private String unicodeToEntities(String str) { StringBuilder strBuf = new StringBuilder(str.length()); /* For each character */ for (int i = 0; i < str.length(); i++) { char ch = str.charAt(i); /* This is a non ASCII, non Whitespace character */ if (!((ch >= 0x0020) && (ch <= 0x007e)) && !Character.isWhitespace(ch)) { strBuf.append("&#x"); //$NON-NLS-1$ String hex = Integer.toHexString(ch & 0xFFFF); if (hex.length() == 2) strBuf.append("00"); //$NON-NLS-1$ strBuf.append(hex).append(";"); //$NON-NLS-1$ } /* This is an ASCII character */ else { strBuf.append(ch); } } return strBuf.toString(); } /** * @return the port used by this server. */ public int getPort() { return SOCKET_PORT; } /** * @return the host used by this server. */ public String getHost() { return LOCALHOST; } }