/** * (C) Copyright 2013 Jabylon (http://www.jabylon.org) and others. * * 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.eclipse.org/legal/epl-v10.html */ package org.jabylon.rest.api; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.text.MessageFormat; import java.util.Locale; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.ServletInputStream; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.core.internal.preferences.Base64; import org.eclipse.emf.cdo.util.CommitException; import org.eclipse.emf.common.util.EList; import org.eclipse.emf.common.util.URI; import org.jabylon.cdo.connector.Modification; import org.jabylon.cdo.connector.TransactionUtil; import org.jabylon.common.util.PreferencesUtil; import org.jabylon.properties.DiffKind; import org.jabylon.properties.Project; import org.jabylon.properties.ProjectLocale; import org.jabylon.properties.ProjectVersion; import org.jabylon.properties.PropertiesFactory; import org.jabylon.properties.PropertiesPackage; import org.jabylon.properties.PropertyFileDescriptor; import org.jabylon.properties.PropertyFileDiff; import org.jabylon.properties.Resolvable; import org.jabylon.properties.Workspace; import org.jabylon.properties.util.PropertyResourceUtil; import org.jabylon.resources.persistence.PropertyPersistenceService; import org.jabylon.rest.api.json.DefaultPermissionCallback; import org.jabylon.rest.api.json.JSONEmitter; import org.jabylon.security.CommonPermissions; import org.jabylon.security.auth.AuthenticationService; import org.jabylon.users.User; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Function; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.util.concurrent.UncheckedExecutionException; /** * TODO short description for ApiServlet. * <p> * Long description for ApiServlet. * * @author utzig */ public class ApiServlet extends HttpServlet implements Function<String, User> // implements Servlet { private static final String BASIC_AUTH_REALM = "BASIC realm=\"Jabylon API\""; private static final String BASIC_PREFIX = "BASIC "; /** field <code>serialVersionUID</code> */ private static final long serialVersionUID = -1167994739560620821L; private Workspace workspace; private PropertyPersistenceService persistence; private AuthenticationService authService; private LoadingCache<String, User> cache; private static final Logger logger = LoggerFactory.getLogger(ApiServlet.class); public ApiServlet(Workspace workspace, AuthenticationService authService, PropertyPersistenceService persistence) { this.workspace = workspace; this.persistence = persistence; this.authService = authService; } /** * @see javax.servlet.Servlet#init(javax.servlet.ServletConfig) */ @Override public void init(ServletConfig config) throws ServletException { cache = CacheBuilder.newBuilder().concurrencyLevel(3).expireAfterAccess(2, TimeUnit.MINUTES).maximumSize(10).build(CacheLoader.from(this)); } /** * @see javax.servlet.Servlet#getServletConfig() */ @Override public ServletConfig getServletConfig() { // TODO Auto-generated method stub return null; } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { logger.info("API request to {}", req.getPathInfo()); Resolvable child = getObject(req.getPathInfo()); if (child == null) { resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Resource " + req.getPathInfo() + " does not exist"); return; } if(!isAuthorized(req, false, child)) { resp.setHeader("WWW-Authenticate", BASIC_AUTH_REALM); resp.sendError(HttpServletResponse.SC_UNAUTHORIZED); } else { JSONEmitter emitter = new JSONEmitter(new DefaultPermissionCallback(getUser(req))); StringBuilder result = new StringBuilder(); String depthString = req.getParameter("depth"); int depth = 1; if (depthString != null) depth = Integer.valueOf(depthString); String type = req.getParameter("type"); if ("file".equals(type)) { if (child instanceof PropertyFileDescriptor) { serveFile((PropertyFileDescriptor) child, resp); } else { serveArchive(child, resp); } } else { // TODO: use appendable emitter.serialize(child, result, depth); resp.getOutputStream().print(result.toString()); } } resp.getOutputStream().close(); } protected Resolvable getObject(String path) throws IOException { String info = path; if (info == null) info = ""; //FIXME: this is for backwards compatibility. Unify this with URI resolver if(info.startsWith("/workspace")) info = info.replaceFirst("/workspace", ""); org.eclipse.emf.common.util.URI uri = org.eclipse.emf.common.util.URI.createURI(info); return workspace.resolveChild(uri); } @Override protected void doPut(final HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { URI uri = URI.createURI(req.getPathInfo()); String[] segmentArray = uri.segments(); if (segmentArray.length == 1) putProject(req, uri, resp); else if (segmentArray.length == 2) putVersion(req, uri, resp); else if (segmentArray.length == 3 && uri.hasTrailingPathSeparator()) putLocale(req, uri, resp); else putPropertyFile(req, uri, resp); } private void putPropertyFile(HttpServletRequest req, URI uri, HttpServletResponse resp) throws IOException { // split between the project/version/locale portion and the rest String[] segmentArray = uri.segments(); String[] projectPart = new String[2]; final String[] descriptorPart = new String[segmentArray.length - projectPart.length]; System.arraycopy(segmentArray, 0, projectPart, 0, projectPart.length); System.arraycopy(segmentArray, projectPart.length, descriptorPart, 0, descriptorPart.length); URI projectURI = URI.createHierarchicalURI(projectPart, null, null); Resolvable version = getObject(projectURI.path()); if (version instanceof ProjectVersion) { if(!isAuthorized(req, true, version)) { resp.setHeader("WWW-Authenticate", BASIC_AUTH_REALM); resp.sendError(HttpServletResponse.SC_UNAUTHORIZED); } else { ProjectVersion projectVersion = (ProjectVersion) version; URI descriptorLocation = URI.createHierarchicalURI(descriptorPart, null, null); File folder = new File(projectVersion.absoluteFilePath().toFileString()); File propertyFile = new File(folder, descriptorLocation.path()); final PropertyFileDiff diff = PropertiesFactory.eINSTANCE.createPropertyFileDiff(); diff.setKind(propertyFile.isFile() ? DiffKind.MODIFY : DiffKind.ADD); updateFile(propertyFile, req.getInputStream()); diff.setNewPath(descriptorLocation.path()); diff.setOldPath(descriptorLocation.path()); try { TransactionUtil.commit(projectVersion, new Modification<ProjectVersion, ProjectVersion>() { @Override public ProjectVersion apply(ProjectVersion object) { object.partialScan(PreferencesUtil.getScanConfigForProject(object.getParent()), diff); return object; } }); //TODO: why not use persistence service to store it instead? persistence.clearCache(); } catch (CommitException e) { resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Commit failed: " + e.getMessage()); logger.error("Commit failed", e); } } } else resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Resource " + projectURI.path() + " does not exist"); } private void putLocale(HttpServletRequest req, final URI uri, HttpServletResponse resp) throws IOException { URI truncated = uri.trimSegments(1); Resolvable object = getObject(truncated.path()); if (object instanceof ProjectVersion) { ProjectVersion version = (ProjectVersion) object; if(!isAuthorized(req, true, version)) { resp.setHeader("WWW-Authenticate", BASIC_AUTH_REALM); resp.sendError(HttpServletResponse.SC_UNAUTHORIZED); } else if (version.getChild(uri.lastSegment()) == null) { try { TransactionUtil.commit(version, new Modification<ProjectVersion, ProjectVersion>() { @Override public ProjectVersion apply(ProjectVersion object) { ProjectLocale locale = PropertiesFactory.eINSTANCE.createProjectLocale(); locale.setName(uri.lastSegment()); locale.setLocale((Locale) PropertiesFactory.eINSTANCE.createFromString(PropertiesPackage.Literals.LOCALE, uri.lastSegment())); PropertyResourceUtil.addNewLocale(locale, object); return object; } }); } catch (CommitException e) { logger.error("Commit failed", e); } } } else resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Version " + truncated.path() + " does not exist"); } private void putVersion(HttpServletRequest req, final URI uri, HttpServletResponse resp) throws IOException { URI truncated = uri.trimSegments(1); Resolvable object = getObject(truncated.path()); if (object instanceof Project) { Project project = (Project) object; if(!isAuthorized(req, true, project)) { resp.setHeader("WWW-Authenticate", BASIC_AUTH_REALM); resp.sendError(HttpServletResponse.SC_UNAUTHORIZED); } else if (project.getChild(uri.lastSegment()) == null) { try { TransactionUtil.commit(project, new Modification<Project, Project>() { @Override public Project apply(Project object) { ProjectVersion child = PropertiesFactory.eINSTANCE.createProjectVersion(); ProjectLocale locale = PropertiesFactory.eINSTANCE.createProjectLocale(); child.getChildren().add(locale); child.setTemplate(locale); child.setName(uri.lastSegment()); object.getChildren().add(child); return object; } }); } catch (CommitException e) { logger.error("Commit failed", e); } } } else resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Project " + truncated.path() + " does not exist"); } private void putProject(HttpServletRequest req, final URI uri, HttpServletResponse resp) throws IOException { // TODO: evaluate JSON stream for settings if(!isAuthorized(req, true, workspace)) { resp.setHeader("WWW-Authenticate", BASIC_AUTH_REALM); resp.sendError(HttpServletResponse.SC_UNAUTHORIZED); } else { try { TransactionUtil.commit(workspace, new Modification<Workspace, Workspace>() { @Override public Workspace apply(Workspace object) { Project child = PropertiesFactory.eINSTANCE.createProject(); child.setName(uri.lastSegment()); object.getChildren().add(child); return object; } }); } catch (CommitException e) { logger.error("Commit failed", e); } } } private void updateFile(File destination, ServletInputStream inputStream) throws IOException { byte[] buffer = new byte[1024]; destination.getParentFile().mkdirs(); FileOutputStream out = new FileOutputStream(destination); try { while (true) { int read = inputStream.read(buffer); if (read > 0) out.write(buffer, 0, read); if (read < 0) break; } } finally { out.close(); inputStream.close(); } } private void serveFile(PropertyFileDescriptor fileDescriptor, HttpServletResponse resp) throws IOException { URI path = fileDescriptor.absolutPath(); File file = new File(path.path()); ServletOutputStream outputStream = resp.getOutputStream(); if (!file.exists()) { resp.setStatus(HttpServletResponse.SC_NOT_FOUND); resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Resource " + fileDescriptor.fullPath() + " does not exist"); } else { resp.setContentLength((int) file.length()); resp.setContentType("application/octet-stream"); writeFileToStream(file, outputStream); } outputStream.flush(); } protected void writeFileToStream(File file, OutputStream out) throws IOException { BufferedInputStream in = null; try { in = new BufferedInputStream(new FileInputStream(file)); byte[] buffer = new byte[1024]; while (true) { int read = in.read(buffer); if (read <= 0) break; out.write(buffer, 0, read); } } finally { if (in != null) in.close(); } } /** * creates an archive that includes all children of the resolvable * @param parent * @param resp * @throws IOException */ private void serveArchive(Resolvable<?,?> parent, HttpServletResponse resp) throws IOException { resp.setContentType("application/zip"); resp.setHeader("Content-disposition",MessageFormat.format("attachment;filename={0}.zip",parent.getName())); ZipOutputStream out = new ZipOutputStream(resp.getOutputStream()); addChildrenToArchive(out,parent); out.close(); resp.flushBuffer(); } @SuppressWarnings("unchecked") private void addChildrenToArchive(ZipOutputStream out, Resolvable<?, ?> parent) throws IOException { EList<Resolvable<?, ?>> children = (EList<Resolvable<?, ?>>) parent.getChildren(); for (Resolvable<?, ?> child : children) { if (child instanceof PropertyFileDescriptor) { PropertyFileDescriptor descriptor = (PropertyFileDescriptor) child; File file = new File(descriptor.absoluteFilePath().path()); if(!file.exists()) continue; out.putNextEntry(new ZipEntry(((PropertyFileDescriptor) child).getLocation().path())); writeFileToStream(file, out); } else addChildrenToArchive(out, child); } } /** * @see javax.servlet.Servlet#getServletInfo() */ @Override public String getServletInfo() { // TODO Auto-generated method stub return null; } /** * @see javax.servlet.Servlet#destroy() */ @Override public void destroy() { cache = null; } protected boolean isAuthorized(HttpServletRequest request, boolean isEdit, Resolvable<?, ?> target) { User user = getUser(request); if(user==null) return false; return CommonPermissions.hasPermission(user, target, isEdit ? CommonPermissions.ACTION_EDIT : CommonPermissions.ACTION_VIEW); } protected User getUser(HttpServletRequest request) { String auth = request.getHeader("Authorization"); if (auth == null) { //no auth header -> anonymous return authService.getAnonymousUser(); } if (!auth.toUpperCase().startsWith(BASIC_PREFIX)) { return null; // we only do BASIC so far } // Encoded user and password come after "BASIC " String userpassEncoded = auth.substring(BASIC_PREFIX.length()); try { return cache.get(userpassEncoded); } catch (ExecutionException e) { // user is not known or not authorized } catch (UncheckedExecutionException e) { // user is not known or not authorized } return null; } private User authenticate(final String username, final String password) { return authService.authenticateUser(username, password); } @Override public User apply(String authHeader) { byte[] decoded = Base64.decode(authHeader.getBytes()); String userpassDecoded = new String(decoded); String[] userPass = userpassDecoded.split(":"); User result = null; if(userPass.length==2) { String username = userPass[0].isEmpty() ? null : userPass[0]; String password = userPass[1]; result = authenticate(username, password); } else if(userPass.length==1) { //must be an auth token result = authenticate(null, userPass[0]); } if(result!=null) return result; throw new IllegalArgumentException("Invalid Credentials"); } }