diff --git a/config/alfresco/web-api-application-context.xml b/config/alfresco/web-api-application-context.xml new file mode 100644 index 0000000000..1d21a27c68 --- /dev/null +++ b/config/alfresco/web-api-application-context.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/java/org/alfresco/web/api/APIRequest.java b/source/java/org/alfresco/web/api/APIRequest.java index cf251dab9d..72f0868a0d 100644 --- a/source/java/org/alfresco/web/api/APIRequest.java +++ b/source/java/org/alfresco/web/api/APIRequest.java @@ -37,6 +37,16 @@ public class APIRequest extends HttpServletRequestWrapper // TODO: Complete list... } + /** + * Enumeration of "required" Authentication level + */ + public enum RequiredAuthentication + { + None, + Guest, + User + } + /** * Construct diff --git a/source/java/org/alfresco/web/api/APIService.java b/source/java/org/alfresco/web/api/APIService.java index 715ea13cc1..ab962215b7 100644 --- a/source/java/org/alfresco/web/api/APIService.java +++ b/source/java/org/alfresco/web/api/APIService.java @@ -18,8 +18,6 @@ package org.alfresco.web.api; import java.io.IOException; -import javax.servlet.ServletContext; - /** * API Service * @@ -29,13 +27,26 @@ public interface APIService { /** - * Initialise service + * Gets the required authentication level for execution of this service * - * @param context + * @return the required authentication level */ - public void init(ServletContext context); - + public APIRequest.RequiredAuthentication getRequiredAuthentication(); + /** + * Gets the HTTP method this service is bound to + * + * @return HTTP method + */ + public APIRequest.HttpMethod getHttpMethod(); + + /** + * Gets the HTTP uri this service is bound to + * + * @return HTTP uri + */ + public String getHttpUri(); + /** * Execute service * diff --git a/source/java/org/alfresco/web/api/APIServiceMap.java b/source/java/org/alfresco/web/api/APIServiceMap.java new file mode 100644 index 0000000000..7701ba67ef --- /dev/null +++ b/source/java/org/alfresco/web/api/APIServiceMap.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * 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.alfresco.web.api; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.aopalliance.intercept.MethodInterceptor; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.context.ApplicationContext; + + +/** + * Registry of Web API Services methods + * + * @author davidc + */ +public class APIServiceMap +{ + // TODO: Support different kinds of uri resolution (e.g. regex:/search/.*) + + private List methods = new ArrayList(); + private List uris = new ArrayList(); + private List services = new ArrayList(); + + + /** + * Construct list of API Services + * + * @param context + */ + public APIServiceMap(ApplicationContext context) + { + // locate authentication interceptor + MethodInterceptor authInterceptor = (MethodInterceptor)context.getBean("web.api.Authentication"); + + // register all API Services + // NOTE: An API Service is one registered in Spring which supports the APIService interface + Map apiServices = context.getBeansOfType(APIService.class, false, false); + for (Map.Entry apiService : apiServices.entrySet()) + { + APIService service = apiService.getValue(); + APIRequest.HttpMethod method = service.getHttpMethod(); + String httpUri = service.getHttpUri(); + if (httpUri == null || httpUri.length() == 0) + { + throw new APIException("Web API Service " + apiService.getKey() + " does not specify a HTTP URI mapping"); + } + + if (authInterceptor != null && service.getRequiredAuthentication() != APIRequest.RequiredAuthentication.None) + { + // wrap API Service in appropriate interceptors (e.g. authentication) + ProxyFactory authFactory = new ProxyFactory(service); + authFactory.addAdvice(authInterceptor); + service = (APIService)authFactory.getProxy(); + } + + methods.add(method); + uris.add(httpUri); + services.add(service); + } + } + + + /** + * Gets an API Service given an HTTP Method and URI + * + * @param method + * @param uri + * @return + */ + public APIService get(APIRequest.HttpMethod method, String uri) + { + APIService apiService = null; + + // TODO: Replace with more efficient approach + for (int i = 0; i < services.size(); i++) + { + if (methods.get(i).equals(method) && uris.get(i).equals(uri)) + { + apiService = services.get(i); + break; + } + } + + return apiService; + } + +} diff --git a/source/java/org/alfresco/web/api/APIServlet.java b/source/java/org/alfresco/web/api/APIServlet.java index 15b62cfe07..ed968bdc9e 100644 --- a/source/java/org/alfresco/web/api/APIServlet.java +++ b/source/java/org/alfresco/web/api/APIServlet.java @@ -17,13 +17,14 @@ package org.alfresco.web.api; import java.io.IOException; -import java.util.regex.Pattern; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.alfresco.web.app.servlet.BaseServlet; +import org.springframework.context.ApplicationContext; +import org.springframework.web.context.support.WebApplicationContextUtils; /** @@ -35,31 +36,23 @@ public class APIServlet extends BaseServlet { private static final long serialVersionUID = 4209892938069597860L; - // API Services - // TODO: Define via configuration - // TODO: Provide mechanism to construct service specific urls (ideally from template) - private static Pattern TEXT_SEARCH_DESCRIPTION_URI = Pattern.compile("/search/textsearchdescription.xml"); - private static Pattern SEARCH_URI = Pattern.compile("/search/text"); - private static APIService TEXT_SEARCH_DESCRIPTION_SERVICE; - private static APIService TEXT_SEARCH_SERVICE; + private APIServiceMap apiServiceMap; @Override public void init() throws ServletException { super.init(); - - // TODO: Replace with dispatch mechanism (maybe lazy construct) - TEXT_SEARCH_DESCRIPTION_SERVICE = new TextSearchDescriptionService(); - TEXT_SEARCH_DESCRIPTION_SERVICE.init(getServletContext()); - TEXT_SEARCH_SERVICE = new TextSearchService(); - TEXT_SEARCH_SERVICE.init(getServletContext()); + + // Retrieve all web api services and index by http url & http method + ApplicationContext context = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext()); + apiServiceMap = new APIServiceMap(context); } // TODO: -// - authentication +// - authentication (as suggested in http://www.xml.com/pub/a/2003/12/17/dive.html) // - atom // - generator // - author (authenticated) @@ -79,31 +72,24 @@ public class APIServlet extends BaseServlet APIRequest request = new APIRequest(req); APIResponse response = new APIResponse(res); - // TODO: Handle authentication - HTTP Auth? - - // // Execute appropriate service // - // TODO: Replace with configurable dispatch mechanism based on HTTP method & uri. // TODO: Handle errors (with appropriate HTTP error responses) APIRequest.HttpMethod method = request.getHttpMethod(); String uri = request.getPathInfo(); - if (method == APIRequest.HttpMethod.GET && TEXT_SEARCH_DESCRIPTION_URI.matcher(uri).matches()) + APIService service = apiServiceMap.get(method, uri); + if (service != null) { - TEXT_SEARCH_DESCRIPTION_SERVICE.execute(request, response); - } - else if (method == APIRequest.HttpMethod.GET && SEARCH_URI.matcher(uri).matches()) - { - TEXT_SEARCH_SERVICE.execute(request, response); + service.execute(request, response); } else { response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + // TODO: add appropriate error detail } - } } diff --git a/source/java/org/alfresco/web/api/BasicAuthentication.java b/source/java/org/alfresco/web/api/BasicAuthentication.java new file mode 100644 index 0000000000..2470822696 --- /dev/null +++ b/source/java/org/alfresco/web/api/BasicAuthentication.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * 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.alfresco.web.api; + +import org.alfresco.repo.security.authentication.AuthenticationException; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.util.Base64; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + + +/** + * HTTP Basic Authentication Interceptor + * + * @author davidc + */ +public class BasicAuthentication implements MethodInterceptor +{ + // dependencies + private AuthenticationService authenticationService; + + /** + * @param authenticationService + */ + public void setAuthenticationService(AuthenticationService authenticationService) + { + this.authenticationService = authenticationService; + } + + /* (non-Javadoc) + * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation) + */ + public Object invoke(MethodInvocation invocation) + throws Throwable + { + boolean authorized = false; + Object retVal = null; + Object[] args = invocation.getArguments(); + APIRequest request = (APIRequest)args[0]; + APIService service = (APIService)invocation.getThis(); + + try + { + // + // validate credentials + // + + String authorization = request.getHeader("Authorization"); + if ((authorization == null || authorization.length() == 0) && service.getRequiredAuthentication().equals(APIRequest.RequiredAuthentication.Guest)) + { + // authenticate as guest, if service allows + authenticationService.authenticateAsGuest(); + authorized = true; + } + else if (authorization != null && authorization.length() > 0) + { + try + { + String[] authorizationParts = authorization.split(" "); + if (!authorizationParts[0].equalsIgnoreCase("basic")) + { + throw new APIException("Authorization '" + authorizationParts[0] + "' not supported."); + } + String decodedAuthorisation = new String(Base64.decode(authorizationParts[1])); + String[] parts = decodedAuthorisation.split(":"); + + if (parts.length == 1) + { + // assume a ticket has been passed + authenticationService.validate(parts[0]); + authorized = true; + } + else + { + // assume username and password passed + if (parts[0].equals(AuthenticationUtil.getGuestUserName())) + { + authenticationService.authenticateAsGuest(); + authorized = true; + } + else + { + authenticationService.authenticate(parts[0], parts[1].toCharArray()); + authorized = true; + } + } + } + catch(AuthenticationException e) + { + // failed authentication + } + } + + // + // execute API service or request authorization + // + + if (authorized) + { + retVal = invocation.proceed(); + } + else + { + APIResponse response = (APIResponse)args[1]; + response.setStatus(401); + response.setHeader("WWW-Authenticate", "Basic realm=\"Alfresco\""); + } + } + finally + { + // clear authentication + // TODO: Consider case where authentication is set before this method is called. + // That shouldn't be the case for the web api. + if (authorized) + { + authenticationService.clearCurrentSecurityContext(); + } + } + + return retVal; + } + +} diff --git a/source/java/org/alfresco/web/api/TextSearchService.java b/source/java/org/alfresco/web/api/services/TextSearch.java similarity index 86% rename from source/java/org/alfresco/web/api/TextSearchService.java rename to source/java/org/alfresco/web/api/services/TextSearch.java index e003a75500..7b119d49a9 100644 --- a/source/java/org/alfresco/web/api/TextSearchService.java +++ b/source/java/org/alfresco/web/api/services/TextSearch.java @@ -1,439 +1,482 @@ -/* - * Copyright (C) 2005 Alfresco, Inc. - * - * Licensed under the Mozilla Public License version 1.1 - * with a permitted attribution clause. You may obtain a - * copy of the License at - * - * http://www.alfresco.org/legal/license.txt - * - * 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.alfresco.web.api; - -import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.util.HashMap; -import java.util.Map; - -import javax.servlet.ServletContext; - -import org.alfresco.service.ServiceRegistry; -import org.alfresco.service.cmr.repository.StoreRef; -import org.alfresco.service.cmr.repository.TemplateNode; -import org.alfresco.service.cmr.repository.TemplateService; -import org.alfresco.service.cmr.search.ResultSet; -import org.alfresco.service.cmr.search.SearchService; -import org.alfresco.util.ApplicationContextHelper; -import org.alfresco.util.GUID; -import org.springframework.context.ApplicationContext; -import org.springframework.web.context.support.WebApplicationContextUtils; - - -/** - * Alfresco Text (simple) Search Service - * - * @author davidc - */ -public class TextSearchService implements APIService -{ - // NOTE: startPage and startIndex are 1 offset. - - // search parameters - // TODO: allow configuration of these - private static final StoreRef searchStore = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore"); - private static final int itemsPerPage = 10; - - // dependencies - private ServiceRegistry serviceRegistry; - private SearchService searchService; - private TemplateService templateService; - - - - /* (non-Javadoc) - * @see org.alfresco.web.api.APIService#init(javax.servlet.ServletContext) - */ - public void init(ServletContext context) - { - ApplicationContext appContext = WebApplicationContextUtils.getWebApplicationContext(context); - init(appContext); - } - - /** - * Internal initialisation - * - * @param context - */ - private void init(ApplicationContext context) - { - serviceRegistry = (ServiceRegistry)context.getBean(ServiceRegistry.SERVICE_REGISTRY); - searchService = (SearchService)context.getBean(ServiceRegistry.SEARCH_SERVICE.getLocalName()); - templateService = (TemplateService)context.getBean(ServiceRegistry.TEMPLATE_SERVICE.getLocalName()); - } - - /* (non-Javadoc) - * @see org.alfresco.web.api.APIService#execute(org.alfresco.web.api.APIRequest, org.alfresco.web.api.APIResponse) - */ - public void execute(APIRequest req, APIResponse res) - throws IOException - { - // - // execute the search - // - - String searchTerms = req.getParameter("q"); - String startPageArg = req.getParameter("p"); - int startPage = 1; - try - { - startPage = new Integer(startPageArg); - } - catch(NumberFormatException e) - { - // NOTE: use default startPage - } - - SearchResult results = search(searchTerms, startPage); - - // - // render the results - // - - String contentType = APIResponse.HTML_TYPE; - String template = HTML_TEMPLATE; - - // TODO: data-drive this - String format = req.getParameter("format"); - if (format != null) - { - if (format.equals("atom")) - { - contentType = APIResponse.ATOM_TYPE; - template = ATOM_TEMPLATE; - } - } - - // execute template - Map searchModel = new HashMap(7, 1.0f); - searchModel.put("request", req); - searchModel.put("search", results); - res.setContentType(contentType + ";charset=UTF-8"); - templateService.processTemplateString(null, template, searchModel, res.getWriter()); - } - - /** - * Execute the search - * - * @param searchTerms - * @param startPage - * @return - */ - private SearchResult search(String searchTerms, int startPage) - { - SearchResult searchResult = null; - ResultSet results = null; - - try - { - // Construct search statement - String[] terms = searchTerms.split(" "); - Map statementModel = new HashMap(7, 1.0f); - statementModel.put("terms", terms); - String query = templateService.processTemplateString(null, QUERY_STATEMENT, statementModel); - results = searchService.query(searchStore, SearchService.LANGUAGE_LUCENE, query); - - int totalResults = results.length(); - int totalPages = (totalResults / itemsPerPage); - totalPages += (totalResults % itemsPerPage != 0) ? 1 : 0; - - // are we out-of-range - if (totalPages != 0 && (startPage < 1 || startPage > totalPages)) - { - throw new APIException("Start page " + startPage + " is outside boundary of " + totalPages + " pages"); - } - - searchResult = new SearchResult(); - searchResult.setSearchTerms(searchTerms); - searchResult.setItemsPerPage(itemsPerPage); - searchResult.setStartPage(startPage); - searchResult.setTotalPages(totalPages); - searchResult.setTotalResults(totalResults); - searchResult.setStartIndex(((startPage -1) * itemsPerPage) + 1); - searchResult.setTotalPageItems(Math.min(itemsPerPage, totalResults - searchResult.getStartIndex() + 1)); - TemplateNode[] nodes = new TemplateNode[searchResult.getTotalPageItems()]; - for (int i = 0; i < searchResult.getTotalPageItems(); i++) - { - nodes[i] = new TemplateNode(results.getNodeRef(i + searchResult.getStartIndex() - 1), serviceRegistry, null); - } - searchResult.setResults(nodes); - - return searchResult; - } - finally - { - if (results != null) - { - results.close(); - } - } - } - - /** - * Search Result - * - * @author davidc - */ - public static class SearchResult - { - private String id; - private String searchTerms; - private int itemsPerPage; - private int totalPages; - private int totalResults; - private int totalPageItems; - private int startPage; - private int startIndex; - private TemplateNode[] results; - - - public int getItemsPerPage() - { - return itemsPerPage; - } - - /*package*/ void setItemsPerPage(int itemsPerPage) - { - this.itemsPerPage = itemsPerPage; - } - - public TemplateNode[] getResults() - { - return results; - } - - /*package*/ void setResults(TemplateNode[] results) - { - this.results = results; - } - - public int getStartIndex() - { - return startIndex; - } - - /*package*/ void setStartIndex(int startIndex) - { - this.startIndex = startIndex; - } - - public int getStartPage() - { - return startPage; - } - - /*package*/ void setStartPage(int startPage) - { - this.startPage = startPage; - } - - public int getTotalPageItems() - { - return totalPageItems; - } - - /*package*/ void setTotalPageItems(int totalPageItems) - { - this.totalPageItems = totalPageItems; - } - - public int getTotalPages() - { - return totalPages; - } - - /*package*/ void setTotalPages(int totalPages) - { - this.totalPages = totalPages; - } - - public int getTotalResults() - { - return totalResults; - } - - /*package*/ void setTotalResults(int totalResults) - { - this.totalResults = totalResults; - } - - public String getSearchTerms() - { - return searchTerms; - } - - /*package*/ void setSearchTerms(String searchTerms) - { - this.searchTerms = searchTerms; - } - - public String getId() - { - if (id == null) - { - id = GUID.generate(); - } - return id; - } - } - - - // TODO: place into accessible file - private final static String ATOM_TEMPLATE = - "<#assign dateformat=\"yyyy-MM-dd\">" + - "<#assign timeformat=\"HH:mm:sszzz\">" + - "\n" + - "\n" + - " Alfresco Search: ${search.searchTerms}\n" + - " 2003-12-13T18:30:02Z\n" + // TODO: - " \n" + - " Alfresco\n" + // TODO: Issuer of search? - " \n" + - " urn:uuid:${search.id}\n" + - " ${search.totalResults}\n" + - " ${search.startIndex}\n" + - " ${search.itemsPerPage}\n" + - " \n" + - " \n" + - " \n" + - " \n" + - "<#if search.startPage > 1>" + - " \n" + - "#if>" + - "<#if search.startPage < search.totalPages>" + - " \n" + - "#if>" + - " \n" + - " \n" + - "<#list search.results as row>" + - " \n" + - " ${row.name}\n" + - " \n" + - " urn:uuid:${row.id}\n" + - " ${row.properties.modified?string(dateformat)}T${row.properties.modified?string(timeformat)}\n" + - " ${row.properties.description}\n" + - " \n" + - "#list>" + - ""; - - // TODO: place into accessible file - private final static String HTML_TEMPLATE = - "\n" + - "\n" + - "\n" + - " \n" + - " Alfresco Text Search: ${search.searchTerms}\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " Alfresco Text Search\n" + - " Results ${search.startIndex} - ${search.startIndex + search.totalPageItems - 1} of ${search.totalResults} for ${search.searchTerms}.\n" + - " \n" + - "<#list search.results as row>" + - " \n" + - " \n" + - " ${row.name}\n" + - " \n" + - " \n" + - " ${row.properties.description}\n" + - " \n" + - " \n" + - "#list>" + - " \n" + - " first" + - "<#if search.startPage > 1>" + - " previous" + - "#if>" + - " ${search.startPage}" + - "<#if search.startPage < search.totalPages>" + - " next" + - "#if>" + - " last" + - " \n" + - "\n"; - - // TODO: place into accessible file - private final static String QUERY_STATEMENT = - "( " + - " TYPE:\"{http://www.alfresco.org/model/content/1.0}content\" AND " + - " (" + - " (" + - "<#list 1..terms?size as i>" + - " @\\{http\\://www.alfresco.org/model/content/1.0\\}name:${terms[i - 1]}" + - "<#if (i < terms?size)>" + - " OR " + - "#if>" + - "#list>" + - " ) " + - " ( " + - "<#list 1..terms?size as i>" + - " TEXT:${terms[i - 1]}" + - "<#if (i < terms?size)>" + - " OR " + - "#if>" + - "#list>" + - " )" + - " )" + - ")"; - - - - /** - * Simple test that can be executed outside of web context - * - * TODO: Move to test harness - * - * @param args - * @throws Exception - */ - public static void main(String[] args) - throws Exception - { - ApplicationContext context = ApplicationContextHelper.getApplicationContext(); - TextSearchService method = new TextSearchService(); - method.init(context); - method.test(); - } - - /** - * Simple test that can be executed outside of web context - * - * TODO: Move to test harness - */ - private void test() - { - SearchResult result = search("alfresco tutorial", 1); - - Map searchModel = new HashMap(7, 1.0f); - Map request = new HashMap(); - request.put("servicePath", "http://localhost:8080/alfresco/service"); - request.put("path", "http://localhost:8080/alfresco"); - searchModel.put("request", request); - searchModel.put("search", result); - - StringWriter rendition = new StringWriter(); - PrintWriter writer = new PrintWriter(rendition); - templateService.processTemplateString(null, HTML_TEMPLATE, searchModel, writer); - System.out.println(rendition.toString()); - } - +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * 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.alfresco.web.api.services; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.repository.TemplateNode; +import org.alfresco.service.cmr.repository.TemplateService; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.util.ApplicationContextHelper; +import org.alfresco.util.GUID; +import org.alfresco.web.api.APIException; +import org.alfresco.web.api.APIRequest; +import org.alfresco.web.api.APIResponse; +import org.alfresco.web.api.APIService; +import org.alfresco.web.api.APIRequest.HttpMethod; +import org.alfresco.web.api.APIRequest.RequiredAuthentication; +import org.springframework.context.ApplicationContext; + + +/** + * Alfresco Text (simple) Search Service + * + * @author davidc + */ +public class TextSearch implements APIService +{ + // NOTE: startPage and startIndex are 1 offset. + + // search parameters + // TODO: allow configuration of these + private static final StoreRef searchStore = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore"); + private static final int itemsPerPage = 10; + + // dependencies + private String uri; + private ServiceRegistry serviceRegistry; + private SearchService searchService; + private TemplateService templateService; + + + /** + * Sets the Http URI + * + * @param uri + */ + public void setHttpUri(String uri) + { + this.uri = uri; + } + + /** + * @param serviceRegistry + */ + public void setServiceRegistry(ServiceRegistry serviceRegistry) + { + this.serviceRegistry = serviceRegistry; + } + + /** + * @param searchService + */ + public void setSearchService(SearchService searchService) + { + this.searchService = searchService; + } + + /** + * @param templateService + */ + public void setTemplateService(TemplateService templateService) + { + this.templateService = templateService; + } + + /* (non-Javadoc) + * @see org.alfresco.web.api.APIService#getRequiredAuthentication() + */ + public RequiredAuthentication getRequiredAuthentication() + { + return APIRequest.RequiredAuthentication.User; + } + + /* (non-Javadoc) + * @see org.alfresco.web.api.APIService#getHttpMethod() + */ + public HttpMethod getHttpMethod() + { + return APIRequest.HttpMethod.GET; + } + + /* (non-Javadoc) + * @see org.alfresco.web.api.APIService#getHttpUri() + */ + public String getHttpUri() + { + return this.uri; + } + + /* (non-Javadoc) + * @see org.alfresco.web.api.APIService#execute(org.alfresco.web.api.APIRequest, org.alfresco.web.api.APIResponse) + */ + public void execute(APIRequest req, APIResponse res) + throws IOException + { + // + // execute the search + // + + String searchTerms = req.getParameter("q"); + String startPageArg = req.getParameter("p"); + int startPage = 1; + try + { + startPage = new Integer(startPageArg); + } + catch(NumberFormatException e) + { + // NOTE: use default startPage + } + + SearchResult results = search(searchTerms, startPage); + + // + // render the results + // + + String contentType = APIResponse.HTML_TYPE; + String template = HTML_TEMPLATE; + + // TODO: data-drive this + String format = req.getParameter("format"); + if (format != null) + { + if (format.equals("atom")) + { + contentType = APIResponse.ATOM_TYPE; + template = ATOM_TEMPLATE; + } + } + + // execute template + Map searchModel = new HashMap(7, 1.0f); + searchModel.put("request", req); + searchModel.put("search", results); + res.setContentType(contentType + ";charset=UTF-8"); + templateService.processTemplateString(null, template, searchModel, res.getWriter()); + } + + /** + * Execute the search + * + * @param searchTerms + * @param startPage + * @return + */ + private SearchResult search(String searchTerms, int startPage) + { + SearchResult searchResult = null; + ResultSet results = null; + + try + { + // Construct search statement + String[] terms = searchTerms.split(" "); + Map statementModel = new HashMap(7, 1.0f); + statementModel.put("terms", terms); + String query = templateService.processTemplateString(null, QUERY_STATEMENT, statementModel); + results = searchService.query(searchStore, SearchService.LANGUAGE_LUCENE, query); + + int totalResults = results.length(); + int totalPages = (totalResults / itemsPerPage); + totalPages += (totalResults % itemsPerPage != 0) ? 1 : 0; + + // are we out-of-range + if (totalPages != 0 && (startPage < 1 || startPage > totalPages)) + { + throw new APIException("Start page " + startPage + " is outside boundary of " + totalPages + " pages"); + } + + searchResult = new SearchResult(); + searchResult.setSearchTerms(searchTerms); + searchResult.setItemsPerPage(itemsPerPage); + searchResult.setStartPage(startPage); + searchResult.setTotalPages(totalPages); + searchResult.setTotalResults(totalResults); + searchResult.setStartIndex(((startPage -1) * itemsPerPage) + 1); + searchResult.setTotalPageItems(Math.min(itemsPerPage, totalResults - searchResult.getStartIndex() + 1)); + TemplateNode[] nodes = new TemplateNode[searchResult.getTotalPageItems()]; + for (int i = 0; i < searchResult.getTotalPageItems(); i++) + { + nodes[i] = new TemplateNode(results.getNodeRef(i + searchResult.getStartIndex() - 1), serviceRegistry, null); + } + searchResult.setResults(nodes); + + return searchResult; + } + finally + { + if (results != null) + { + results.close(); + } + } + } + + /** + * Search Result + * + * @author davidc + */ + public static class SearchResult + { + private String id; + private String searchTerms; + private int itemsPerPage; + private int totalPages; + private int totalResults; + private int totalPageItems; + private int startPage; + private int startIndex; + private TemplateNode[] results; + + + public int getItemsPerPage() + { + return itemsPerPage; + } + + /*package*/ void setItemsPerPage(int itemsPerPage) + { + this.itemsPerPage = itemsPerPage; + } + + public TemplateNode[] getResults() + { + return results; + } + + /*package*/ void setResults(TemplateNode[] results) + { + this.results = results; + } + + public int getStartIndex() + { + return startIndex; + } + + /*package*/ void setStartIndex(int startIndex) + { + this.startIndex = startIndex; + } + + public int getStartPage() + { + return startPage; + } + + /*package*/ void setStartPage(int startPage) + { + this.startPage = startPage; + } + + public int getTotalPageItems() + { + return totalPageItems; + } + + /*package*/ void setTotalPageItems(int totalPageItems) + { + this.totalPageItems = totalPageItems; + } + + public int getTotalPages() + { + return totalPages; + } + + /*package*/ void setTotalPages(int totalPages) + { + this.totalPages = totalPages; + } + + public int getTotalResults() + { + return totalResults; + } + + /*package*/ void setTotalResults(int totalResults) + { + this.totalResults = totalResults; + } + + public String getSearchTerms() + { + return searchTerms; + } + + /*package*/ void setSearchTerms(String searchTerms) + { + this.searchTerms = searchTerms; + } + + public String getId() + { + if (id == null) + { + id = GUID.generate(); + } + return id; + } + } + + + // TODO: place into accessible file + private final static String ATOM_TEMPLATE = + "<#assign dateformat=\"yyyy-MM-dd\">" + + "<#assign timeformat=\"HH:mm:sszzz\">" + + "\n" + + "\n" + + " Alfresco Search: ${search.searchTerms}\n" + + " 2003-12-13T18:30:02Z\n" + // TODO: + " \n" + + " Alfresco\n" + // TODO: Issuer of search? + " \n" + + " urn:uuid:${search.id}\n" + + " ${search.totalResults}\n" + + " ${search.startIndex}\n" + + " ${search.itemsPerPage}\n" + + " \n" + + " \n" + + " \n" + + " \n" + + "<#if search.startPage > 1>" + + " \n" + + "#if>" + + "<#if search.startPage < search.totalPages>" + + " \n" + + "#if>" + + " \n" + + " \n" + + "<#list search.results as row>" + + " \n" + + " ${row.name}\n" + + " \n" + + " urn:uuid:${row.id}\n" + + " ${row.properties.modified?string(dateformat)}T${row.properties.modified?string(timeformat)}\n" + + " ${row.properties.description}\n" + + " \n" + + "#list>" + + ""; + + // TODO: place into accessible file + private final static String HTML_TEMPLATE = + "\n" + + "\n" + + "\n" + + " \n" + + " Alfresco Text Search: ${search.searchTerms}\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " Alfresco Text Search\n" + + " Results ${search.startIndex} - ${search.startIndex + search.totalPageItems - 1} of ${search.totalResults} for ${search.searchTerms}.\n" + + " \n" + + "<#list search.results as row>" + + " \n" + + " \n" + + " ${row.name}\n" + + " \n" + + " \n" + + " ${row.properties.description}\n" + + " \n" + + " \n" + + "#list>" + + " \n" + + " first" + + "<#if search.startPage > 1>" + + " previous" + + "#if>" + + " ${search.startPage}" + + "<#if search.startPage < search.totalPages>" + + " next" + + "#if>" + + " last" + + " \n" + + "\n"; + + // TODO: place into accessible file + private final static String QUERY_STATEMENT = + "( " + + " TYPE:\"{http://www.alfresco.org/model/content/1.0}content\" AND " + + " (" + + " (" + + "<#list 1..terms?size as i>" + + " @\\{http\\://www.alfresco.org/model/content/1.0\\}name:${terms[i - 1]}" + + "<#if (i < terms?size)>" + + " OR " + + "#if>" + + "#list>" + + " ) " + + " ( " + + "<#list 1..terms?size as i>" + + " TEXT:${terms[i - 1]}" + + "<#if (i < terms?size)>" + + " OR " + + "#if>" + + "#list>" + + " )" + + " )" + + ")"; + + + + /** + * Simple test that can be executed outside of web context + * + * TODO: Move to test harness + * + * @param args + * @throws Exception + */ + public static void main(String[] args) + throws Exception + { + ApplicationContext context = ApplicationContextHelper.getApplicationContext(); + TextSearch method = new TextSearch(); + method.setServiceRegistry((ServiceRegistry)context.getBean(ServiceRegistry.SERVICE_REGISTRY)); + method.setTemplateService((TemplateService)context.getBean(ServiceRegistry.TEMPLATE_SERVICE.getLocalName())); + method.setSearchService((SearchService)context.getBean(ServiceRegistry.SEARCH_SERVICE.getLocalName())); + method.setHttpUri("/search/text"); + method.test(); + } + + /** + * Simple test that can be executed outside of web context + * + * TODO: Move to test harness + */ + private void test() + { + SearchResult result = search("alfresco tutorial", 1); + + Map searchModel = new HashMap(7, 1.0f); + Map request = new HashMap(); + request.put("servicePath", "http://localhost:8080/alfresco/service"); + request.put("path", "http://localhost:8080/alfresco"); + searchModel.put("request", request); + searchModel.put("search", result); + + StringWriter rendition = new StringWriter(); + PrintWriter writer = new PrintWriter(rendition); + templateService.processTemplateString(null, HTML_TEMPLATE, searchModel, writer); + System.out.println(rendition.toString()); + } + } \ No newline at end of file diff --git a/source/java/org/alfresco/web/api/TextSearchDescriptionService.java b/source/java/org/alfresco/web/api/services/TextSearchDescription.java similarity index 63% rename from source/java/org/alfresco/web/api/TextSearchDescriptionService.java rename to source/java/org/alfresco/web/api/services/TextSearchDescription.java index 6fa5e1ed4b..3da8ad77ec 100644 --- a/source/java/org/alfresco/web/api/TextSearchDescriptionService.java +++ b/source/java/org/alfresco/web/api/services/TextSearchDescription.java @@ -1,75 +1,110 @@ -/* - * Copyright (C) 2005 Alfresco, Inc. - * - * Licensed under the Mozilla Public License version 1.1 - * with a permitted attribution clause. You may obtain a - * copy of the License at - * - * http://www.alfresco.org/legal/license.txt - * - * 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.alfresco.web.api; - -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; - -import javax.servlet.ServletContext; - -import org.alfresco.service.ServiceRegistry; -import org.alfresco.service.cmr.repository.TemplateService; -import org.springframework.context.ApplicationContext; -import org.springframework.web.context.support.WebApplicationContextUtils; - - -/** - * Provide OpenSearch Description for an Alfresco Text (simple) Search - * - * @author davidc - */ -public class TextSearchDescriptionService implements APIService -{ - // dependencies - private TemplateService templateService; - - /* (non-Javadoc) - * @see org.alfresco.web.api.APIService#init(javax.servlet.ServletContext) - */ - public void init(ServletContext context) - { - ApplicationContext appContext = WebApplicationContextUtils.getWebApplicationContext(context); - templateService = (TemplateService)appContext.getBean(ServiceRegistry.TEMPLATE_SERVICE.getLocalName()); - } - - /* (non-Javadoc) - * @see org.alfresco.web.api.APIService#execute(org.alfresco.web.api.APIRequest, org.alfresco.web.api.APIResponse) - */ - public void execute(APIRequest req, APIResponse res) - throws IOException - { - // create model for open search template - Map model = new HashMap(7, 1.0f); - model.put("request", req); - - // execute template - res.setContentType(APIResponse.OPEN_SEARCH_DESCRIPTION_TYPE + ";charset=UTF-8"); - templateService.processTemplateString(null, OPEN_SEARCH_DESCRIPTION, model, res.getWriter()); - } - - // TODO: place into accessible file - private final static String OPEN_SEARCH_DESCRIPTION = - "\n" + - "\n" + - " Alfresco Text Search\n" + - " Search all of Alfresco Company Home via text keywords\n" + - " \n" + - " \n" + - ""; - -} +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * 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.alfresco.web.api.services; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.service.cmr.repository.TemplateService; +import org.alfresco.web.api.APIRequest; +import org.alfresco.web.api.APIResponse; +import org.alfresco.web.api.APIService; +import org.alfresco.web.api.APIRequest.HttpMethod; +import org.alfresco.web.api.APIRequest.RequiredAuthentication; + + +/** + * Provide OpenSearch Description for an Alfresco Text (simple) Search + * + * @author davidc + */ +public class TextSearchDescription implements APIService +{ + // dependencies + private String uri; + private TemplateService templateService; + + /** + * Sets the Http URI + * + * @param uri + */ + public void setHttpUri(String uri) + { + this.uri = uri; + } + + /** + * @param templateService + */ + public void setTemplateService(TemplateService templateService) + { + this.templateService = templateService; + } + + + /* (non-Javadoc) + * @see org.alfresco.web.api.APIService#getRequiredAuthentication() + */ + public RequiredAuthentication getRequiredAuthentication() + { + return APIRequest.RequiredAuthentication.None; + } + + /* (non-Javadoc) + * @see org.alfresco.web.api.APIService#getHttpMethod() + */ + public HttpMethod getHttpMethod() + { + return APIRequest.HttpMethod.GET; + } + + /* (non-Javadoc) + * @see org.alfresco.web.api.APIService#getHttpUri() + */ + public String getHttpUri() + { + return this.uri; + } + + /* (non-Javadoc) + * @see org.alfresco.web.api.APIService#execute(org.alfresco.web.api.APIRequest, org.alfresco.web.api.APIResponse) + */ + public void execute(APIRequest req, APIResponse res) + throws IOException + { + // create model for open search template + Map model = new HashMap(7, 1.0f); + model.put("request", req); + + // execute template + res.setContentType(APIResponse.OPEN_SEARCH_DESCRIPTION_TYPE + ";charset=UTF-8"); + templateService.processTemplateString(null, OPEN_SEARCH_DESCRIPTION, model, res.getWriter()); + } + + // TODO: place into accessible file + private final static String OPEN_SEARCH_DESCRIPTION = + "\n" + + "\n" + + " Alfresco Text Search\n" + + " Search all of Alfresco Company Home via text keywords\n" + + " \n" + + " \n" + + ""; + +} diff --git a/source/web/WEB-INF/web.xml b/source/web/WEB-INF/web.xml index 97ecd5ecb1..9010f6d9fc 100644 --- a/source/web/WEB-INF/web.xml +++ b/source/web/WEB-INF/web.xml @@ -59,6 +59,7 @@ classpath:alfresco/web-client-application-context.xml classpath:web-services-application-context.xml + classpath:alfresco/web-api-application-context.xml classpath:alfresco/application-context.xml Spring config file locations