/* * RHQ Management Platform * Copyright (C) 2005-2013 Red Hat, Inc. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License, version 2, as * published by the Free Software Foundation, and/or the GNU Lesser * General Public License, version 2.1, also as published by the Free * Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License and the GNU Lesser General Public License * for more details. * * You should have received a copy of the GNU General Public License * and the GNU Lesser General Public License along with this program; * if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.rhq.enterprise.server.rest; import java.io.IOException; import java.io.StringWriter; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.net.URI; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.PostConstruct; import javax.ejb.EJB; import javax.ws.rs.Produces; import javax.ws.rs.core.CacheControl; import javax.ws.rs.core.GenericEntity; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.infinispan.Cache; import org.infinispan.manager.CacheContainer; import org.rhq.core.domain.auth.Subject; import org.rhq.core.domain.measurement.DataType; import org.rhq.core.domain.measurement.MeasurementDefinition; import org.rhq.core.domain.measurement.MeasurementSchedule; import org.rhq.core.domain.resource.Resource; import org.rhq.core.domain.resource.ResourceType; import org.rhq.core.domain.resource.group.GroupCategory; import org.rhq.core.domain.resource.group.ResourceGroup; import org.rhq.core.domain.util.PageControl; import org.rhq.core.domain.util.PageList; import org.rhq.enterprise.server.resource.ResourceManagerLocal; import org.rhq.enterprise.server.resource.group.ResourceGroupManagerLocal; import org.rhq.enterprise.server.rest.domain.GroupRest; import org.rhq.enterprise.server.rest.domain.Link; import org.rhq.enterprise.server.rest.domain.MetricSchedule; import org.rhq.enterprise.server.rest.domain.PagingCollection; import org.rhq.enterprise.server.rest.domain.ResourceWithType; import freemarker.cache.ClassTemplateLoader; import freemarker.cache.MultiTemplateLoader; import freemarker.cache.TemplateLoader; import freemarker.template.Template; import freemarker.template.TemplateException; /** * Abstract base class for EJB classes that implement REST methods. * For the cache and its eviction policies see standalone-full.xml (in * the RHQ Server's AS7/standalone/configuration directory, as modified * by the installer.) * * @author Heiko W. Rupp * @author Jay Shaughnessy */ @Produces({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML,MediaType.TEXT_HTML,"application/vnd.rhq.wrapped+json"}) @javax.annotation.Resource(name = "ISPN", mappedName = "java:jboss/infinispan/rhq") @SuppressWarnings("unchecked") public class AbstractRestBean { protected Log log = LogFactory.getLog(getClass().getName()); protected final MediaType wrappedCollectionJsonType = new MediaType("application","vnd.rhq.wrapped+json"); protected final String wrappedCollectionJson = "application/vnd.rhq.wrapped+json"; private static final CacheKey META_KEY = new CacheKey("rhq.rest.resourceMeta", 0); @javax.annotation.Resource( name = "ISPN") private CacheContainer container; protected Cache<CacheKey, Object> cache; /** Subject of the caller that gets injected via {@link SetCallerInterceptor} */ protected Subject caller; @EJB protected ResourceManagerLocal resMgr; @EJB protected ResourceGroupManagerLocal resourceGroupManager; @PostConstruct public void start() { this.cache = this.container.getCache("rhqRestCache"); } /** * Renders the passed object with the help of a freemarker template into a string. Freemarket templates * are searched in the class path in a directory called "/rest_templates". In the usual Maven tree structure, * this is below src/main/resources/. * * @param templateName Template to use for rendering. If the template name does not end in .ftl, .ftl is appended. * @param objectToRender Object to render via template * @return Template filled with data from objectToRender */ protected String renderTemplate(String templateName, Object objectToRender) { try { freemarker.template.Configuration config = new freemarker.template.Configuration(); // XXX fall-over to ClassTL after failure in FTL seems not to work ClassTemplateLoader ctl = new ClassTemplateLoader(getClass(), "/rest_templates/"); TemplateLoader[] loaders = new TemplateLoader[] { ctl }; MultiTemplateLoader mtl = new MultiTemplateLoader(loaders); config.setTemplateLoader(mtl); if (!templateName.endsWith(".ftl")) { templateName = templateName + ".ftl"; } Template template = config.getTemplate(templateName); StringWriter out = new StringWriter(); try { Map<String, Object> root = new HashMap<String, Object>(); root.put("var", objectToRender); template.process(root, out); return out.toString(); } finally { out.close(); } } catch (IOException ioe) { log.error(ioe); } catch (TemplateException te) { log.error(te.getMessage()); } return null; } /** * Retrieve an object from the cache. This is identified by its class and id * @param id Id of the object to load. * @param clazz Wanted return type * @return Object if found and the caller has access to it. * @see #getFromCache(int, Class) */ protected <T> T getFromCache(int id, Class<T> clazz) { CacheKey key = new CacheKey(clazz, id); return getFromCache(key, clazz); } /** * Retrieve an object from the cache if present or null otherwise. * We need to be careful here as we must not return objects the current * caller has no access to. We do this by checking the "readers" attribute * of the selected node to see if the caller has put the object there * @param key FullyQualified name (=path in cache) of the object to retrieve * @param clazz Return type * @return The desired object if found and valid for the current caller. Null otherwise. * @see #putToCache(CacheKey, Object) */ protected <T> T getFromCache(CacheKey key, Class<T> clazz) { Object o = null; CacheValue value = (CacheValue) cache.get(key); boolean debugEnabled = log.isDebugEnabled(); if (null != value) { if (debugEnabled) { log.debug("Cache Hit for " + key); } if (value.getReaders().contains(caller.getId())) { o = value.getValue(); } else { if (debugEnabled) { log.debug("Cache Hit ignored, caller " + caller.toString() + " not found"); } } } else { if (debugEnabled) { log.debug("Cache Miss for " + key); } } return (T) o; } /** * Put an object into the cache identified by its type and id * @param id Id of the object to put * @param clazz Type to put in * @param o Object to put * @return true if put was successful * @see #putToCache(CacheKey, Object) */ protected <T> boolean putToCache(int id, Class<T> clazz, T o) { CacheKey key = new CacheKey(clazz, id); return putToCache(key, o); } /** * Put an object into the cache. We need to record the caller so that we can later * check if the caller can access that object or not. * @param key Fully qualified name (=path to object) * @param o Object to put * @return true if put was successful * @see #getFromCache(CacheKey, Class) */ @SuppressWarnings("unchecked") protected <T> boolean putToCache(CacheKey key, T o) { boolean result = false; CacheValue value = (CacheValue) cache.get(key); if (null != value) { value.getReaders().add(caller.getId()); value.setValue(o); } else { value = new CacheValue(o, caller.getId()); } try { cache.put(key, value); if (log.isDebugEnabled()) { log.debug("Cache Put " + key); } result = true; } catch (Exception e) { log.warn(e.getMessage()); } return result; } /** * Remove an item from the cache * @param id Id of the item * @param clazz Type of object for that node * @return true if object is no longer in cache */ protected <T> boolean removeFromCache(int id, Class<T> clazz) { CacheKey key = new CacheKey(clazz, id); Object cacheValue = cache.remove(key); if (null != cacheValue) { log.debug("Cache Remove " + key); } return true; } public ResourceWithType fillRWT(Resource res, UriInfo uriInfo) { ResourceType resourceType = res.getResourceType(); ResourceWithType rwt = new ResourceWithType(res.getName(), res.getId()); rwt.setTypeName(resourceType.getName()); rwt.setTypeId(resourceType.getId()); rwt.setPluginName(resourceType.getPlugin()); rwt.setStatus(res.getInventoryStatus().name()); rwt.setLocation(res.getLocation()); rwt.setDescription(res.getDescription()); rwt.setAvailability(res.getCurrentAvailability().getAvailabilityType().toString()); Resource parent = res.getParentResource(); if (parent != null) { rwt.setParentId(parent.getId()); } else { rwt.setParentId(0); } rwt.setAncestry(res.getAncestry()); UriBuilder uriBuilder = uriInfo.getBaseUriBuilder(); uriBuilder.path("/operation/definitions"); uriBuilder.queryParam("resourceId", res.getId()); URI uri = uriBuilder.build(); Link link = new Link("operationDefinitions", uri.toString()); rwt.addLink(link); link = getLinkToResource(res, uriInfo, "self"); rwt.addLink(link); uriBuilder = uriInfo.getBaseUriBuilder(); uriBuilder.path("/resource/{id}/schedules"); uri = uriBuilder.build(res.getId()); link = new Link("schedules", uri.toString()); rwt.addLink(link); uriBuilder = uriInfo.getBaseUriBuilder(); uriBuilder.path("/resource/{id}/availability"); uri = uriBuilder.build(res.getId()); link = new Link("availability", uri.toString()); rwt.addLink(link); uriBuilder = uriInfo.getBaseUriBuilder(); uriBuilder.path("/resource/{id}/children"); uri = uriBuilder.build(res.getId()); link = new Link("children", uri.toString()); rwt.addLink(link); uriBuilder = uriInfo.getBaseUriBuilder(); uriBuilder.path("/resource/{id}/alerts"); uri = uriBuilder.build(res.getId()); link = new Link("alerts", uri.toString()); rwt.addLink(link); uriBuilder = uriInfo.getBaseUriBuilder(); uriBuilder.path("/alert/definitions"); uriBuilder.queryParam("resourceId",res.getId()); uri = uriBuilder.build(res.getId()); link = new Link("alertDefinitions", uri.toString()); rwt.addLink(link); if (parent != null) { uriBuilder = uriInfo.getBaseUriBuilder(); uriBuilder.path("/resource/{id}/"); uri = uriBuilder.build(parent.getId()); link = new Link("parent", uri.toString()); rwt.addLink(link); } rwt.addLink(createUILink(uriInfo,UILinkTemplate.RESOURCE,res.getId())); return rwt; } protected Link getLinkToResource(Resource res, UriInfo uriInfo, String rel) { UriBuilder uriBuilder;URI uri; Link link; uriBuilder = uriInfo.getBaseUriBuilder(); uriBuilder.path("/resource/{id}"); uri = uriBuilder.build(res.getId()); link = new Link(rel, uri.toString()); return link; } protected Link getLinkToResourceType(ResourceType type, UriInfo uriInfo, String rel) { UriBuilder uriBuilder;URI uri; Link link; uriBuilder = uriInfo.getBaseUriBuilder(); uriBuilder.path("/resource/type/{id}"); uri = uriBuilder.build(type.getId()); link = new Link(rel, uri.toString()); return link; } protected Link getLinkToGroup(ResourceGroup group, UriInfo uriInfo, String rel) { UriBuilder uriBuilder; URI uri; Link link; uriBuilder = uriInfo.getBaseUriBuilder(); uriBuilder.path("/group/{id}"); uri = uriBuilder.build(group.getId()); link = new Link(rel, uri.toString()); return link; } protected Resource fetchResource(int resourceId) { Resource res; res = resMgr.getResource(caller, resourceId); if (res == null) throw new StuffNotFoundException("Resource with id " + resourceId); /* res = getFromCache(resourceId, Resource.class); if (res == null) { res = resMgr.getResource(caller, resourceId); if (res != null) putToCache(resourceId, Resource.class, res); else throw new StuffNotFoundException("Resource with id " + resourceId); } */ return res; } protected Response.ResponseBuilder withMediaType(Response.ResponseBuilder builder, HttpHeaders headers) { MediaType mediaType = headers.getAcceptableMediaTypes().get(0); builder.type(mediaType); return builder; } /** * Creates a response builder with "ok" response containing the provided list and applied pagination * as prescribed by the {@code pageList}. * @param headers the http headers * @param uriInfo uri info * @param pageList the "original" list coming from the EJB layer containing the paging info * @param results the result list with REST-ready entities * @param elementType the type of the entities contained in the result list * @param <T> the type of the entities contained in the result list * @return the builder for the response */ protected <T> Response.ResponseBuilder paginate(HttpHeaders headers, UriInfo uriInfo, PageList<?> pageList, List<T> results, final Class<T> elementType) { Response.ResponseBuilder builder = Response.ok(); withMediaType(builder, headers); MediaType mediaType = headers.getAcceptableMediaTypes().get(0); if (mediaType.equals(wrappedCollectionJsonType)) { wrapForPaging(builder, uriInfo, pageList, results); } else { ParameterizedType myType = new ParameterizedType() { final Type[] params = new Type[] { elementType }; @Override public Type[] getActualTypeArguments() { return params; } @Override public Type getRawType() { return List.class; } @Override public Type getOwnerType() { return null; } }; GenericEntity<List<T>> list = new GenericEntity<List<T>>(results, myType); builder.entity(list); createPagingHeader(builder,uriInfo,pageList); } return builder; } /** * Create the paging headers for collections and attach them to the passed builder. Those are represented as * <i>Link:</i> http headers that carry the URL for the pages and the respective relation. * <br/>In addition a <i>X-total-size</i> header is created that contains the whole collection size. * <p/> * If you need no further "building" of the response apart from paginating, you should look into using * the {@link #paginate(javax.ws.rs.core.HttpHeaders, javax.ws.rs.core.UriInfo, org.rhq.core.domain.util.PageList, * java.util.List, Class)}. * @param builder The ResponseBuilder that receives the headers * @param uriInfo The uriInfo of the incoming request to build the urls * @param resultList The collection with its paging information */ protected void createPagingHeader(final Response.ResponseBuilder builder, final UriInfo uriInfo, final PageList<?> resultList) { UriBuilder uriBuilder; PageControl pc = resultList.getPageControl(); int page = pc.getPageNumber(); if (resultList.getTotalSize()> (pc.getPageNumber() +1 ) * pc.getPageSize()) { int nextPage = page+1; uriBuilder = uriInfo.getRequestUriBuilder(); // adds ?q, ?ps and ?category if needed uriBuilder.replaceQueryParam("page",nextPage); builder.header("Link",new Link("next",uriBuilder.build().toString()).rfc5988String()); } if (page>0) { int prevPage = page -1; uriBuilder = uriInfo.getRequestUriBuilder(); // adds ?q, ?ps and ?category if needed uriBuilder.replaceQueryParam("page",prevPage); builder.header("Link", new Link("prev",uriBuilder.build().toString()).rfc5988String()); } // A link to the last page if (!pc.isUnlimited()) { int lastPage = (resultList.getTotalSize() / pc.getPageSize() ) -1; uriBuilder = uriInfo.getRequestUriBuilder(); // adds ?q, ?ps and ?category if needed uriBuilder.replaceQueryParam("page",lastPage); builder.header("Link", new Link("last",uriBuilder.build().toString()).rfc5988String()); } // A link to the current page uriBuilder = uriInfo.getRequestUriBuilder(); // adds ?q, ?ps and ?category if needed builder.header("Link", new Link("current",uriBuilder.build().toString()).rfc5988String()); // Create a total size header builder.header("X-collection-size",resultList.getTotalSize()); } /** * Wrap the passed collection #resultList in an object with paging information * <p/> * If you need no further "building" of the response apart from paginating, you should look into using * the {@link #paginate(javax.ws.rs.core.HttpHeaders, javax.ws.rs.core.UriInfo, org.rhq.core.domain.util.PageList, * java.util.List, Class)}. * * @param builder ResonseBuilder to add the entity to * @param uriInfo UriInfo to construct paging links * @param originalList The original list to obtain the paging info from * @param resultList The list of result items */ protected <T> void wrapForPaging(Response.ResponseBuilder builder, UriInfo uriInfo, final PageList<?> originalList, final Collection<T> resultList) { PagingCollection<T> pColl = new PagingCollection<T>(resultList); pColl.setTotalSize(originalList.getTotalSize()); PageControl pageControl = originalList.getPageControl(); pColl.setPageSize(pageControl.getPageSize()); int page = pageControl.getPageNumber(); pColl.setCurrentPage(page); int lastPage = (originalList.getTotalSize() / pageControl.getPageSize()) -1 ; // -1 as page # is 0 based pColl.setLastPage(lastPage); UriBuilder uriBuilder; if (originalList.getTotalSize() > (page +1 ) * pageControl.getPageSize()) { int nextPage = page +1; uriBuilder = uriInfo.getRequestUriBuilder(); // adds ?q, ?ps and ?category if needed uriBuilder.replaceQueryParam("page",nextPage); pColl.addLink(new Link("next",uriBuilder.build().toString())); } if (page > 0) { int prevPage = page -1; uriBuilder = uriInfo.getRequestUriBuilder(); // adds ?q, ?ps and ?category if needed uriBuilder.replaceQueryParam("page",prevPage); pColl.addLink(new Link("prev",uriBuilder.build().toString())); } // A link to the last page if (!pageControl.isUnlimited()) { uriBuilder = uriInfo.getRequestUriBuilder(); // adds ?q, ?ps and ?category if needed uriBuilder.replaceQueryParam("page",lastPage); pColl.addLink( new Link("last",uriBuilder.build().toString())); } // A link to the current page uriBuilder = uriInfo.getRequestUriBuilder(); // adds ?q, ?ps and ?category if needed pColl.addLink(new Link("current",uriBuilder.build().toString())); builder.entity(pColl); } /** * Fetch the group with the passed id * * @param groupId id of the resource group * @param requireCompatible Does the group have to be a compatible group? * @return the group object if found * @throws org.rhq.enterprise.server.rest.StuffNotFoundException if the group is not found (or not accessible by the caller) * @throws BadArgumentException if a compatible group is required, but the found one is not a compatible one */ protected ResourceGroup fetchGroup(int groupId, boolean requireCompatible) { ResourceGroup resourceGroup; resourceGroup = resourceGroupManager.getResourceGroup(caller, groupId); if (resourceGroup == null) { throw new StuffNotFoundException("Group with id " + groupId); } if (requireCompatible) { if (resourceGroup.getGroupCategory() != GroupCategory.COMPATIBLE) { throw new BadArgumentException("Group with id " + groupId,"it is no compatible group"); } } return resourceGroup; } protected GroupRest fillGroup(ResourceGroup group, UriInfo uriInfo) { GroupRest gr = new GroupRest(group.getName()); gr.setId(group.getId()); gr.setCategory(group.getGroupCategory()); gr.setRecursive(group.isRecursive()); if (group.getGroupDefinition()!=null) { gr.setDynaGroupDefinitionId(group.getGroupDefinition().getId()); } int expCount = resourceGroupManager.getExplicitGroupMemberCount(group.getId()); int impCount = expCount; if (group.isRecursive() && expCount > 0) { impCount = resourceGroupManager.getImplicitGroupMemberCount(group.getId()); } gr.setExplicitCount(expCount); gr.setImplicitCount(impCount); UriBuilder uriBuilder = uriInfo.getBaseUriBuilder(); uriBuilder.path("/group/{id}"); URI uri = uriBuilder.build(group.getId()); Link link = new Link("edit",uri.toASCIIString()); gr.getLinks().add(link); gr.getLinks().add(getLinkToGroup(group,uriInfo, "self")); uriBuilder = uriInfo.getBaseUriBuilder(); uriBuilder.path("/group/{id}/metricDefinitions"); uri = uriBuilder.build(group.getId()); link = new Link("metricDefinitions",uri.toASCIIString()); gr.getLinks().add(link); gr.getLinks().add(createUILink(uriInfo,UILinkTemplate.GROUP,group.getId())); return gr; } /** * Creates a link to the respective entry in coregui * @param uriInfo The uriInfo object to build the final url from * @param template Template to use * @param entityId Ids of the various entities used in the template * @return A Link object */ protected Link createUILink(UriInfo uriInfo, UILinkTemplate template, Integer... entityId) { String urlBase = template.getUrl(); String replaced = String.format(urlBase, entityId); UriBuilder uriBuilder = uriInfo.getBaseUriBuilder(); uriBuilder.fragment(replaced); uriBuilder.replacePath("coregui/"); // trailing / is needed URI uri = uriBuilder.build(); String href = uri.toString(); href = href.replaceAll("%2F","/"); Link link = new Link("coregui", href); return link; } protected MetricSchedule getMetricScheduleInternal(UriInfo uriInfo, MeasurementSchedule schedule, MeasurementDefinition definition) { MetricSchedule ms = new MetricSchedule(schedule.getId(), definition.getName(), definition.getDisplayName(), schedule.isEnabled(), schedule.getInterval(), definition .getUnits().toString(), definition.getDataType().toString()); ms.setDefinitionId(definition.getId()); if (schedule.getMtime()!=null) ms.setMtime(schedule.getMtime()); UriBuilder uriBuilder; URI uri; if (definition.getDataType() == DataType.MEASUREMENT) { uriBuilder = uriInfo.getBaseUriBuilder(); uriBuilder.path("/metric/data/{id}"); uri = uriBuilder.build(schedule.getId()); Link metricLink = new Link("metric", uri.toString()); ms.addLink(metricLink); uriBuilder = uriInfo.getBaseUriBuilder(); uriBuilder.path("/metric/data/{id}/raw"); uri = uriBuilder.build(schedule.getId()); metricLink = new Link("metric-raw", uri.toString()); ms.addLink(metricLink); } // create link to the resource uriBuilder = uriInfo.getBaseUriBuilder(); uriBuilder.path("resource/" + schedule.getResource().getId()); uri = uriBuilder.build(); Link link = new Link("resource", uri.toString()); ms.addLink(link); return ms; } /** * Set the caching header on the response * @param builder Response builder to put the caching header on * @param maxAgeSecs Max retention time on the client. Only set if the value is > 0 */ protected void setCachingHeader(Response.ResponseBuilder builder, int maxAgeSecs) { CacheControl cc = new CacheControl(); cc.setPrivate(false); cc.setNoCache(false); cc.setNoStore(false); if (maxAgeSecs>-1) cc.setMaxAge(maxAgeSecs); builder.cacheControl(cc); } protected static class CacheKey { private String namespace; private int id; /** * @param clazz The class name will be used as the namespace for the id. * @param id */ public CacheKey(Class<?> clazz, int id) { this(clazz.getName(), id); } public CacheKey(String namespace, int id) { this.namespace = namespace; this.id = id; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((namespace == null) ? 0 : namespace.hashCode()); result = prime * result + id; return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } CacheKey other = (CacheKey) obj; if (namespace == null) { if (other.namespace != null) { return false; } } else if (!namespace.equals(other.namespace)) { return false; } if (id != other.id) { return false; } return true; } @Override public String toString() { return "CacheKey [namespace=" + namespace + ", id=" + id + "]"; } } private static class CacheValue { private Object value; private Set<Integer> readers; public CacheValue(Object value, int readerId) { this.readers = new HashSet<Integer>(); this.readers.add(readerId); this.value = value; } public Object getValue() { return value; } public void setValue(Object value) { this.value = value; } public Set<Integer> getReaders() { return readers; } } }