/*
* Copyright 2005-2007 Noelios Consulting.
*
* The contents of this file are subject to the terms of the Common Development
* and Distribution License (the "License"). You may not use this file except in
* compliance with the License.
*
* You can obtain a copy of the license at
* http://www.opensource.org/licenses/cddl1.txt See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each file and
* include the License file at http://www.opensource.org/licenses/cddl1.txt If
* applicable, add the following below this CDDL HEADER, with the fields
* enclosed by brackets "[]" replaced with your own identifying information:
* Portions Copyright [yyyy] [name of copyright owner]
*/
package com.noelios.restlet;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.restlet.Application;
import org.restlet.Client;
import org.restlet.Component;
import org.restlet.Context;
import org.restlet.Directory;
import org.restlet.Server;
import org.restlet.data.CharacterSet;
import org.restlet.data.ClientInfo;
import org.restlet.data.Form;
import org.restlet.data.Language;
import org.restlet.data.MediaType;
import org.restlet.data.Parameter;
import org.restlet.data.Preference;
import org.restlet.data.Protocol;
import org.restlet.data.Request;
import org.restlet.data.Response;
import org.restlet.resource.Representation;
import org.restlet.resource.Resource;
import org.restlet.resource.Variant;
import org.restlet.util.Helper;
import com.noelios.restlet.application.ApplicationHelper;
import com.noelios.restlet.component.ComponentHelper;
import com.noelios.restlet.http.StreamClientHelper;
import com.noelios.restlet.http.StreamServerHelper;
import com.noelios.restlet.local.DirectoryResource;
import com.noelios.restlet.util.FormUtils;
/**
* Restlet factory supported by the engine.
*
* @author Jerome Louvel (contact@noelios.com)
*/
public class Engine extends org.restlet.util.Engine {
/** Obtain a suitable logger. */
private static Logger logger = Logger.getLogger(Engine.class
.getCanonicalName());
/** Complete version. */
public static final String VERSION = org.restlet.util.Engine.VERSION;
/** Complete version header. */
public static final String VERSION_HEADER = "Noelios-Restlet-Engine/"
+ VERSION;
/**
* Registers a new Noelios Restlet Engine.
*/
public static void register() {
Engine.setInstance(new Engine());
}
/** List of available client connectors. */
private List<ConnectorHelper> registeredClients;
/** List of available server connectors. */
private List<ConnectorHelper> registeredServers;
/**
* Constructor that will automatically attempt to discover connectors.
*/
@SuppressWarnings("unchecked")
public Engine() {
this(true);
}
/**
* Constructor.
*
* @param discoverConnectors
* True if connectors should be automatically discovered.
*/
@SuppressWarnings("unchecked")
public Engine(boolean discoverConnectors) {
if (discoverConnectors) {
// Find the factory class name
String line = null;
String provider = null;
// Find the factory class name
ClassLoader cl = org.restlet.util.Engine.getClassLoader();
URL configURL;
// Register the client connector providers
try {
for (Enumeration<URL> configUrls = cl
.getResources("META-INF/services/com.noelios.restlet.ClientHelper"); configUrls
.hasMoreElements();) {
configURL = configUrls.nextElement();
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(
configURL.openStream(), "utf-8"));
line = reader.readLine();
while (line != null) {
provider = getProviderClassName(line);
if ((provider != null) && (!provider.equals(""))) {
// Instantiate the factory
try {
Class<? extends ConnectorHelper> providerClass = (Class<? extends ConnectorHelper>) Class
.forName(provider);
getRegisteredClients().add(
providerClass.getConstructor(
Client.class).newInstance(
(Client) null));
} catch (Exception e) {
logger.log(Level.SEVERE,
"Unable to register the client connector "
+ provider, e);
}
}
line = reader.readLine();
}
} catch (IOException e) {
logger.log(Level.SEVERE,
"Unable to read the provider descriptor: "
+ configURL.toString());
} finally {
if (reader != null)
reader.close();
}
}
} catch (IOException ioe) {
logger
.log(
Level.SEVERE,
"Exception while detecting the client connectors.",
ioe);
}
// Register the server connector providers
try {
for (Enumeration<URL> configUrls = cl
.getResources("META-INF/services/com.noelios.restlet.ServerHelper"); configUrls
.hasMoreElements();) {
configURL = configUrls.nextElement();
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(
configURL.openStream(), "utf-8"));
line = reader.readLine();
while (line != null) {
provider = getProviderClassName(line);
if ((provider != null) && (!provider.equals(""))) {
// Instantiate the factory
try {
Class<? extends ConnectorHelper> providerClass = (Class<? extends ConnectorHelper>) Class
.forName(provider);
getRegisteredServers().add(
providerClass.getConstructor(
Server.class).newInstance(
(Server) null));
} catch (Exception e) {
logger.log(Level.SEVERE,
"Unable to register the server connector "
+ provider, e);
}
}
line = reader.readLine();
}
} catch (IOException e) {
logger.log(Level.SEVERE,
"Unable to read the provider descriptor: "
+ configURL.toString());
} finally {
if (reader != null)
reader.close();
}
}
} catch (IOException ioe) {
logger
.log(
Level.SEVERE,
"Exception while detecting the client connectors.",
ioe);
}
// Register the default connectors that will be used if no
// other connector has been found
getRegisteredClients().add(new StreamClientHelper(null));
getRegisteredServers().add(new StreamServerHelper(null));
}
}
/**
* Returns the list of available client connectors.
*
* @return The list of available client connectors.
*/
public List<ConnectorHelper> getRegisteredClients() {
if (this.registeredClients == null)
this.registeredClients = new ArrayList<ConnectorHelper>();
return this.registeredClients;
}
/**
* Returns the list of available server connectors.
*
* @return The list of available server connectors.
*/
public List<ConnectorHelper> getRegisteredServers() {
if (this.registeredServers == null)
this.registeredServers = new ArrayList<ConnectorHelper>();
return this.registeredServers;
}
/**
* Creates a directory resource.
*
* @param handler
* The parent directory handler.
* @param request
* The request to handle.
* @param response
* The response to return.
* @return A new directory resource.
* @throws IOException
*/
public Resource createDirectoryResource(Directory handler, Request request,
Response response) throws IOException {
return new DirectoryResource(handler, request, response);
}
/**
* Creates a new helper for a given component.
*
* @param application
* The application to help.
* @param parentContext
* The parent context, typically the component's context.
* @return The new helper.
*/
public Helper createHelper(Application application, Context parentContext) {
return new ApplicationHelper(application, parentContext);
}
/**
* Creates a new helper for a given client connector.
*
* @param client
* The client to help.
* @return The new helper.
*/
public Helper createHelper(Client client) {
Helper result = null;
if (client.getProtocols().size() > 0) {
ConnectorHelper connector = null;
for (Iterator<ConnectorHelper> iter = getRegisteredClients()
.iterator(); (result == null) && iter.hasNext();) {
connector = iter.next();
if (connector.getProtocols().containsAll(client.getProtocols())) {
try {
result = connector.getClass().getConstructor(
Client.class).newInstance(client);
} catch (Exception e) {
logger
.log(
Level.SEVERE,
"Exception while instantiation the client connector.",
e);
}
}
}
if (result == null) {
// Couldn't find a matching connector
StringBuilder sb = new StringBuilder();
sb
.append("No available client connector supports the required protocols: ");
for (Protocol p : client.getProtocols()) {
sb.append(p.getName()).append(" ");
}
sb
.append(". Please add the JAR of a matching connector to your classpath.");
logger.log(Level.WARNING, sb.toString());
}
}
return result;
}
/**
* Creates a new helper for a given component.
*
* @param component
* The component to help.
* @return The new helper.
*/
public Helper createHelper(Component component) {
return new ComponentHelper(component);
}
/**
* Creates a new helper for a given server connector.
*
* @param server
* The server to help.
* @return The new helper.
*/
public Helper createHelper(Server server) {
Helper result = null;
if (server.getProtocols().size() > 0) {
ConnectorHelper connector = null;
for (Iterator<ConnectorHelper> iter = getRegisteredServers()
.iterator(); (result == null) && iter.hasNext();) {
connector = iter.next();
if (connector.getProtocols().containsAll(server.getProtocols())) {
try {
result = connector.getClass().getConstructor(
Server.class).newInstance(server);
} catch (Exception e) {
logger
.log(
Level.SEVERE,
"Exception while instantiation the server connector.",
e);
}
}
}
if (result == null) {
// Couldn't find a matching connector
StringBuilder sb = new StringBuilder();
sb
.append("No available server connector supports the required protocols: ");
for (Protocol p : server.getProtocols()) {
sb.append(p.getName()).append(" ");
}
sb
.append(". Please add the JAR of a matching connector to your classpath.");
logger.log(Level.WARNING, sb.toString());
}
}
return result;
}
/**
* Parses the "java.version" system property and returns the first digit of
* the version number of the Java Runtime Environment (e.g. "1" for
* "1.3.0").
*
* @see <a href="http://java.sun.com/j2se/versioning_naming.html">Official
* Java versioning</a>
* @return The major version number of the Java Runtime Environment.
*/
public static int getJavaMajorVersion() {
int result;
String javaVersion = System.getProperty("java.version");
try {
result = Integer.parseInt(javaVersion.substring(0, javaVersion
.indexOf(".")));
} catch (Exception e) {
result = 0;
}
return result;
}
/**
* Parses the "java.version" system property and returns the second digit of
* the version number of the Java Runtime Environment (e.g. "3" for
* "1.3.0").
*
* @see <a href="http://java.sun.com/j2se/versioning_naming.html">Official
* Java versioning</a>
* @return The minor version number of the Java Runtime Environment.
*/
public static int getJavaMinorVersion() {
int result;
String javaVersion = System.getProperty("java.version");
try {
result = Integer.parseInt(javaVersion.split("\\.")[1]);
} catch (Exception e) {
result = 0;
}
return result;
}
/**
* Parses the "java.version" system property and returns the update release
* number of the Java Runtime Environment (e.g. "10" for "1.3.0_10").
*
* @see <a href="http://java.sun.com/j2se/versioning_naming.html">Official
* Java versioning</a>
* @return The release number of the Java Runtime Environment or 0 if it
* does not exist.
*/
public static int getJavaUpdateVersion() {
int result;
String javaVersion = System.getProperty("java.version");
try {
result = Integer.parseInt(javaVersion.substring(javaVersion
.indexOf('_') + 1));
} catch (Exception e) {
result = 0;
}
return result;
}
/**
* Returns the preferred variant representation for a given resource
* according the the client preferences.
*
* @param client
* The client preferences.
* @param variants
* The list of variants to compare.
* @return The preferred variant.
* @see <a
* href="http://httpd.apache.org/docs/2.2/en/content-negotiation.html#algorithm">Apache
* content negotiation algorithm</a>
*/
public Variant getPreferredVariant(ClientInfo client,
List<Variant> variants, Language defaultLanguage) {
if (variants == null) {
return null;
} else {
List<Language> variantLanguages = null;
MediaType variantMediaType = null;
boolean compatibleLanguage = false;
boolean compatibleMediaType = false;
Variant currentVariant = null;
Variant bestVariant = null;
Preference<Language> currentLanguagePref = null;
Preference<Language> bestLanguagePref = null;
Preference<MediaType> currentMediaTypePref = null;
Preference<MediaType> bestMediaTypePref = null;
float bestQuality = 0;
float bestLanguageScore = 0;
float bestMediaTypeScore = 0;
// If no language preference is defined or even none matches, we
// want to make sure that at least a variant can be returned.
// Based on experience, it appears that browsers are often
// misconfigured and don't expose all the languages actually
// understood by end users.
// Thus, a few other preferences are added to the user's ones:
// - primary languages inferred from and sorted according to the
// user's preferences with quality between 0.005 and 0.006
// - default language (if any) with quality 0.003
// - primary language of the default language (if available) with
// quality 0.002
// - all languages with quality 0.001
List<Preference<Language>> languagePrefs = client
.getAcceptedLanguages();
List<Preference<Language>> primaryLanguagePrefs = new ArrayList<Preference<Language>>();
// A default language preference is defined with a better weight
// than the "All languages" preference
Preference<Language> defaultLanguagePref = ((defaultLanguage == null) ? null
: new Preference<Language>(defaultLanguage, 0.003f));
Preference<Language> allLanguagesPref = new Preference<Language>(
Language.ALL, 0.001f);
if (languagePrefs.isEmpty()) {
// All languages accepted.
languagePrefs.add(new Preference<Language>(Language.ALL));
} else {
// Get the primary language preferences that are not currently
// accepted by the client
List<String> list = new ArrayList<String>();
for (Preference<Language> preference : languagePrefs) {
Language language = preference.getMetadata();
if (!language.getSubTags().isEmpty()) {
if (!list.contains(language.getPrimaryTag())) {
list.add(language.getPrimaryTag());
primaryLanguagePrefs
.add(new Preference<Language>(new Language(
language.getPrimaryTag()),
0.005f + (0.001f * preference
.getQuality())));
}
}
}
// If the default language is a "primary" language but is not
// present in the list of all primary languages, add it.
if (defaultLanguage != null
&& !defaultLanguage.getSubTags().isEmpty()) {
if (!list.contains(defaultLanguage.getPrimaryTag())) {
primaryLanguagePrefs.add(new Preference<Language>(
new Language(defaultLanguage.getPrimaryTag()),
0.002f));
}
}
}
// Client preferences are altered
languagePrefs.addAll(primaryLanguagePrefs);
if (defaultLanguagePref != null) {
languagePrefs.add(defaultLanguagePref);
// In this case, if the client adds the "all languages"
// preference, the latter is removed, in order to support the
// default preference defined by the server
List<Preference<Language>> list = new ArrayList<Preference<Language>>();
for (Preference<Language> preference : languagePrefs) {
Language language = preference.getMetadata();
if (!language.equals(Language.ALL)) {
list.add(preference);
}
}
languagePrefs = list;
}
languagePrefs.add(allLanguagesPref);
// For each available variant, we will compute the negotiation score
// which is dependant on the language score and on the media type
// score
for (Iterator<Variant> iter1 = variants.iterator(); iter1.hasNext();) {
currentVariant = iter1.next();
variantLanguages = currentVariant.getLanguages();
variantMediaType = currentVariant.getMediaType();
// All languages of the current variant are scored.
for (Language variantLanguage : variantLanguages) {
// For each language preference defined in the call
// Calculate the score and remember the best scoring
// preference
for (Iterator<Preference<Language>> iter2 = languagePrefs
.iterator(); (variantLanguage != null)
&& iter2.hasNext();) {
currentLanguagePref = iter2.next();
float currentScore = getScore(variantLanguage,
currentLanguagePref.getMetadata());
boolean compatiblePref = (currentScore != -1.0f);
// 3) Do we have a better preference?
// currentScore *= currentPref.getQuality();
if (compatiblePref
&& ((bestLanguagePref == null) || (currentScore > bestLanguageScore))) {
bestLanguagePref = currentLanguagePref;
bestLanguageScore = currentScore;
}
}
}
// Are the preferences compatible with the current variant
// language?
compatibleLanguage = (variantLanguages.isEmpty())
|| (bestLanguagePref != null);
// If no media type preference is defined, assume that all media
// types are acceptable
List<Preference<MediaType>> mediaTypePrefs = client
.getAcceptedMediaTypes();
if (mediaTypePrefs.size() == 0)
mediaTypePrefs
.add(new Preference<MediaType>(MediaType.ALL));
// For each media range preference defined in the call
// Calculate the score and remember the best scoring preference
for (Iterator<Preference<MediaType>> iter2 = mediaTypePrefs
.iterator(); compatibleLanguage && iter2.hasNext();) {
currentMediaTypePref = iter2.next();
float currentScore = getScore(variantMediaType,
currentMediaTypePref.getMetadata());
boolean compatiblePref = (currentScore != -1.0f);
// 3) Do we have a better preference?
// currentScore *= currentPref.getQuality();
if (compatiblePref
&& ((bestMediaTypePref == null) || (currentScore > bestMediaTypeScore))) {
bestMediaTypePref = currentMediaTypePref;
bestMediaTypeScore = currentScore;
}
}
// Are the preferences compatible with the current media type?
compatibleMediaType = (variantMediaType == null)
|| (bestMediaTypePref != null);
if (compatibleLanguage && compatibleMediaType) {
// Do we have a compatible media type?
float currentQuality = 0;
if (bestLanguagePref != null) {
currentQuality += (bestLanguagePref.getQuality() * 10F);
} else if (!variantLanguages.isEmpty()) {
currentQuality += 0.1F * 10F;
}
if (bestMediaTypePref != null) {
// So, let's conclude on the current variant, its
// quality
currentQuality += bestMediaTypePref.getQuality();
}
if (bestVariant == null) {
bestVariant = currentVariant;
bestQuality = currentQuality;
} else if (currentQuality > bestQuality) {
bestVariant = currentVariant;
bestQuality = currentQuality;
}
}
// Reset the preference variables
bestLanguagePref = null;
bestLanguageScore = 0;
bestMediaTypePref = null;
bestMediaTypeScore = 0;
}
return bestVariant;
}
}
/**
* Returns a matching score between 2 Languages
*
* @param variantLanguage
* @param preferenceLanguage
* @return the positive matching score or -1 if the languages are not
* compatible
*/
private float getScore(Language variantLanguage, Language preferenceLanguage) {
float score = 0.0f;
boolean compatibleLang = true;
// 1) Compare the main tag
if (variantLanguage.getPrimaryTag().equalsIgnoreCase(
preferenceLanguage.getPrimaryTag())) {
score += 100;
} else if (!preferenceLanguage.getPrimaryTag().equals("*")) {
compatibleLang = false;
} else if (!preferenceLanguage.getSubTags().isEmpty()) {
// Only "*" is an acceptable language range
compatibleLang = false;
} else {
// The valid "*" range has the lowest valid score
score++;
}
if (compatibleLang) {
// 2) Compare the sub tags
if ((preferenceLanguage.getSubTags().isEmpty())
|| (variantLanguage.getSubTags().isEmpty())) {
if (variantLanguage.getSubTags().isEmpty()
&& preferenceLanguage.getSubTags().isEmpty()) {
score += 10;
} else {
// Don't change the score
}
} else {
int maxSize = Math.min(preferenceLanguage.getSubTags().size(),
variantLanguage.getSubTags().size());
for (int i = 0; i < maxSize && compatibleLang; i++) {
if (preferenceLanguage.getSubTags().get(i)
.equalsIgnoreCase(
variantLanguage.getSubTags().get(i))) {
// Each subtag contribution to the score
// is getting less and less important
score += Math.pow(10, 1 - i);
} else {
// SubTags are different
compatibleLang = false;
}
}
}
}
return (compatibleLang ? score : -1.0f);
}
/**
* Returns a matching score between 2 Media types
*
* @param variantMediaType
* @param preferenceMediaType
* @return the positive matching score or -1 if the media types are not
* compatible
*/
private float getScore(MediaType variantMediaType,
MediaType preferenceMediaType) {
float score = 0.0f;
boolean comptabibleMediaType = true;
// 1) Compare the main types
if (preferenceMediaType.getMainType().equals(
variantMediaType.getMainType())) {
score += 1000;
} else if (!preferenceMediaType.getMainType().equals("*")) {
comptabibleMediaType = false;
} else if (!preferenceMediaType.getSubType().equals("*")) {
// Ranges such as "*/html" are not supported
// Only "*/*" is acceptable in this case
comptabibleMediaType = false;
}
if (comptabibleMediaType) {
// 2) Compare the sub types
if (variantMediaType.getSubType().equals(
preferenceMediaType.getSubType())) {
score += 100;
} else if (!preferenceMediaType.getSubType().equals("*")) {
// Subtype are different
comptabibleMediaType = false;
}
if (comptabibleMediaType
&& (variantMediaType.getParameters() != null)) {
// 3) Compare the parameters
// If current media type is compatible with the
// current media range then the parameters need to
// be checked too
for (Iterator iter3 = variantMediaType.getParameters()
.iterator(); iter3.hasNext();) {
Parameter currentParam = (Parameter) iter3.next();
if (isParameterFound(currentParam, preferenceMediaType)) {
score++;
}
}
}
}
return (comptabibleMediaType ? score : -1.0f);
}
/**
* Parses a line to extract the provider class name.
*
* @param line
* The line to parse.
* @return The provider's class name or an empty string.
*/
private String getProviderClassName(String line) {
int index = line.indexOf('#');
if (index != -1)
line = line.substring(0, index);
return line.trim();
}
/**
* Indicates if the searched parameter is specified in the given media
* range.
*
* @param searchedParam
* The searched parameter.
* @param mediaRange
* The media range to inspect.
* @return True if the searched parameter is specified in the given media
* range.
*/
private boolean isParameterFound(Parameter searchedParam,
MediaType mediaRange) {
boolean result = false;
for (Iterator iter = mediaRange.getParameters().iterator(); !result
&& iter.hasNext();) {
result = searchedParam.equals((Parameter) iter.next());
}
return result;
}
/**
* Parses an URL encoded Web form.
*
* @param logger
* The logger to use.
* @param form
* The target form.
* @param webForm
* The posted form.
*/
public void parse(Logger logger, Form form, Representation webForm) {
if (webForm != null) {
FormUtils.parsePost(logger, form, webForm);
}
}
/**
* Parses an URL encoded query string into a given form.
*
* @param logger
* The logger to use.
* @param form
* The target form.
* @param queryString
* Query string.
* @param characterSet
* The supported character encoding.
*/
public void parse(Logger logger, Form form, String queryString,
CharacterSet characterSet) {
if ((queryString != null) && !queryString.equals("")) {
FormUtils.parseQuery(logger, form, queryString, characterSet);
}
}
}