diff --git a/source/java/org/alfresco/web/api/APIException.java b/source/java/org/alfresco/web/api/APIException.java new file mode 100644 index 0000000000..778134a927 --- /dev/null +++ b/source/java/org/alfresco/web/api/APIException.java @@ -0,0 +1,49 @@ +/* + * 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.error.AlfrescoRuntimeException; + +/** + * API Service Exceptions. + * + * @author David Caruana + */ +public class APIException extends AlfrescoRuntimeException +{ + private static final long serialVersionUID = -7338963365877285084L; + + public APIException(String msgId) + { + super(msgId); + } + + public APIException(String msgId, Throwable cause) + { + super(msgId, cause); + } + + public APIException(String msgId, Object ... args) + { + super(msgId, args); + } + + public APIException(String msgId, Throwable cause, Object ... args) + { + super(msgId, args, cause); + } +} diff --git a/source/java/org/alfresco/web/api/APIRequest.java b/source/java/org/alfresco/web/api/APIRequest.java new file mode 100644 index 0000000000..cf251dab9d --- /dev/null +++ b/source/java/org/alfresco/web/api/APIRequest.java @@ -0,0 +1,82 @@ +/* + * 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 javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; + + +/** + * API Service Request + * + * @author davidc + */ +public class APIRequest extends HttpServletRequestWrapper +{ + + /** + * Enumerartion of HTTP Methods + */ + public enum HttpMethod + { + GET; + // TODO: Complete list... + } + + + /** + * Construct + * + * @param req + */ + public APIRequest(HttpServletRequest req) + { + super(req); + } + + /** + * Gets the HTTP Method + * + * @return Http Method + */ + public HttpMethod getHttpMethod() + { + String method = getMethod().trim().toUpperCase(); + return HttpMethod.valueOf(method); + } + + /** + * Gets the Alfresco Context URL + * + * @return context url e.g. http://localhost:port/alfresco + */ + public String getPath() + { + return getScheme() + "://" + getServerName() + ":" + getServerPort() + getContextPath(); + } + + /** + * Gets the Alfresco Service URL + * + * @return service url e.g. http://localhost:port/alfresco/service + */ + public String getServicePath() + { + return getPath() + getServletPath(); + } + +} diff --git a/source/java/org/alfresco/web/api/APIResponse.java b/source/java/org/alfresco/web/api/APIResponse.java new file mode 100644 index 0000000000..f89cc96d09 --- /dev/null +++ b/source/java/org/alfresco/web/api/APIResponse.java @@ -0,0 +1,46 @@ +/* + * 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 javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; + +/** + * API Service Response + * + * @author davidc + */ +public class APIResponse extends HttpServletResponseWrapper +{ + + // Content Types + + public static final String HTML_TYPE = "text/html"; + public static final String OPEN_SEARCH_DESCRIPTION_TYPE = "application/opensearchdescription+xml"; + public static final String ATOM_TYPE = "application/atom+xml"; + + /** + * Construct + * + * @param res + */ + public APIResponse(HttpServletResponse res) + { + super(res); + } + +} diff --git a/source/java/org/alfresco/web/api/APIService.java b/source/java/org/alfresco/web/api/APIService.java new file mode 100644 index 0000000000..715ea13cc1 --- /dev/null +++ b/source/java/org/alfresco/web/api/APIService.java @@ -0,0 +1,49 @@ +/* + * 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 javax.servlet.ServletContext; + +/** + * API Service + * + * @author davidc + */ +public interface APIService +{ + + /** + * Initialise service + * + * @param context + */ + public void init(ServletContext context); + + + /** + * Execute service + * + * @param req + * @param res + * @throws IOException + */ + public void execute(APIRequest req, APIResponse res) + throws IOException; + +} diff --git a/source/java/org/alfresco/web/api/APIServlet.java b/source/java/org/alfresco/web/api/APIServlet.java new file mode 100644 index 0000000000..15b62cfe07 --- /dev/null +++ b/source/java/org/alfresco/web/api/APIServlet.java @@ -0,0 +1,109 @@ +/* + * 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.regex.Pattern; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.web.app.servlet.BaseServlet; + + +/** + * Entry point for web based services (REST style) + * + * @author davidc + */ +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; + + + @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()); + } + + +// TODO: +// - authentication +// - atom +// - generator +// - author (authenticated) +// - icon +// - html +// - icon + + + /* + * (non-Javadoc) + * + * @see javax.servlet.http.HttpServlet#service(javax.servlet.http.HttpServletRequest, + * javax.servlet.http.HttpServletResponse) + */ + protected void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException + { + 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()) + { + TEXT_SEARCH_DESCRIPTION_SERVICE.execute(request, response); + } + else if (method == APIRequest.HttpMethod.GET && SEARCH_URI.matcher(uri).matches()) + { + TEXT_SEARCH_SERVICE.execute(request, response); + } + else + { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + + } + +} diff --git a/source/java/org/alfresco/web/api/TextSearchDescriptionService.java b/source/java/org/alfresco/web/api/TextSearchDescriptionService.java new file mode 100644 index 0000000000..6fa5e1ed4b --- /dev/null +++ b/source/java/org/alfresco/web/api/TextSearchDescriptionService.java @@ -0,0 +1,75 @@ +/* + * 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" + + ""; + +} diff --git a/source/java/org/alfresco/web/api/TextSearchService.java b/source/java/org/alfresco/web/api/TextSearchService.java new file mode 100644 index 0000000000..a00be0b36c --- /dev/null +++ b/source/java/org/alfresco/web/api/TextSearchService.java @@ -0,0 +1,439 @@ +/* + * 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 search.startPage < search.totalPages>" + + " \n" + + "" + + " \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" + + "" + + ""; + + // 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" + + "" + + "
\n" + + " first" + + "<#if search.startPage > 1>" + + " previous" + + "" + + " ${search.startPage}" + + "<#if search.startPage < search.totalPages>" + + " next" + + "" + + " 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 " + + "" + + "" + + " ) " + + " ( " + + "<#list 1..terms?size as i>" + + " TEXT:${terms[i - 1]}" + + "<#if (i < terms?size)>" + + " OR " + + "" + + "" + + " )" + + " )" + + ")"; + + + + /** + * 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("apiPath", "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/web/WEB-INF/web.xml b/source/web/WEB-INF/web.xml index e95a3d85d8..97ecd5ecb1 100644 --- a/source/web/WEB-INF/web.xml +++ b/source/web/WEB-INF/web.xml @@ -230,6 +230,11 @@ 5 + + apiServlet + org.alfresco.web.api.APIServlet + + workflowDefinitionImageServlet org.alfresco.web.app.servlet.WorkflowDefinitionImageServlet @@ -239,7 +244,7 @@ JBPMDeployProcessServlet org.alfresco.web.app.servlet.JBPMDeployProcessServlet - + Faces Servlet /faces/* @@ -300,6 +305,11 @@ /webdav/* + + apiServlet + /service/* + + JBPMDeployProcessServlet /jbpm/deployprocess diff --git a/source/web/services/index.html b/source/web/services/index.html new file mode 100644 index 0000000000..d73f945b71 --- /dev/null +++ b/source/web/services/index.html @@ -0,0 +1,19 @@ + + + Alfresco Web Services API (REST style) + + + + +

Alfresco Web Services API (REST style)

+ + Alfresco Text Search (OpenSearch description) +
+
+