package org.ovirt.engine.api.restapi.resource; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Response; import org.ovirt.engine.api.model.BaseResource; import org.ovirt.engine.api.model.CreationStatus; import org.ovirt.engine.api.model.Host; import org.ovirt.engine.api.model.Link; import org.ovirt.engine.api.restapi.logging.Messages; import org.ovirt.engine.api.restapi.types.Mapper; import org.ovirt.engine.api.restapi.util.LinkHelper; import org.ovirt.engine.api.rsdl.ServiceTree; import org.ovirt.engine.api.rsdl.ServiceTreeNode; import org.ovirt.engine.api.utils.LinkCreator; import org.ovirt.engine.core.common.action.VdcReturnValueBase; import org.ovirt.engine.core.common.businessentities.AsyncTaskStatus; import org.ovirt.engine.core.common.businessentities.VDS; import org.ovirt.engine.core.common.job.Job; import org.ovirt.engine.core.common.job.JobExecutionStatus; import org.ovirt.engine.core.common.queries.GetTasksStatusesByTasksIDsParameters; import org.ovirt.engine.core.common.queries.IdQueryParameters; import org.ovirt.engine.core.common.queries.NameQueryParameters; import org.ovirt.engine.core.common.queries.VdcQueryParametersBase; import org.ovirt.engine.core.common.queries.VdcQueryReturnValue; import org.ovirt.engine.core.common.queries.VdcQueryType; import org.ovirt.engine.core.compat.Guid; public abstract class AbstractBackendResource<R extends BaseResource, Q /* extends IVdcQueryable */> extends BackendResource { protected static final String ID_SEPARATOR = ","; protected static final long MONITOR_DELAY = 1000L; protected static final javax.ws.rs.core.Response.Status ACCEPTED_STATUS = javax.ws.rs.core.Response.Status.ACCEPTED; protected Class<R> modelType; protected Class<Q> entityType; protected String[] subCollections; public static enum PollingType { VDSM_TASKS, JOB; } protected AbstractBackendResource(Class<R> modelType, Class<Q> entityType) { this.modelType = modelType; this.entityType = entityType; this.subCollections = getSubCollections(); } protected <F, T> Mapper<F, T> getMapper(Class<F> from, Class<T> to) { return mappingLocator.getMapper(from, to); } protected R map(Q entity) { return map(entity, null); } protected R map(Q entity, R template) { return getMapper(entityType, modelType).map(entity, template); } protected Q map(R model) { return map(model, null); } protected Q map(R model, Q template) { return getMapper(modelType, entityType).map(model, template); } protected CreationStatus awaitCompletion(VdcReturnValueBase result) { return awaitCompletion(result, PollingType.VDSM_TASKS); } protected CreationStatus awaitCompletion(VdcReturnValueBase result, PollingType pollingType) { CreationStatus status = null; while (incomplete(status = getAsynchronousStatus(result, pollingType))) { delay(MONITOR_DELAY); } return status; } protected CreationStatus getAsynchronousStatus(VdcReturnValueBase result) { return getVdsmTasksStatus(result); } protected CreationStatus getAsynchronousStatus(VdcReturnValueBase result, PollingType pollingType) { CreationStatus asyncStatus = null; if (pollingType==PollingType.JOB) { asyncStatus = getJobIdStatus(result); } else if (pollingType==PollingType.VDSM_TASKS){ asyncStatus = getVdsmTasksStatus(result); } else { throw new IllegalStateException("Unexpected Polling Status"); } return asyncStatus; } private CreationStatus getVdsmTasksStatus(VdcReturnValueBase result) { CreationStatus asyncStatus = null; VdcQueryReturnValue monitorResult = runQuery(VdcQueryType.GetTasksStatusesByTasksIDs, new GetTasksStatusesByTasksIDsParameters(result.getVdsmTaskIdList())); if (monitorResult != null && monitorResult.getSucceeded() && monitorResult.getReturnValue() != null) { Mapper<AsyncTaskStatus, CreationStatus> mapper = getMapper(AsyncTaskStatus.class, CreationStatus.class); for (AsyncTaskStatus task : asCollection(AsyncTaskStatus.class, monitorResult.getReturnValue())) { asyncStatus = mapper.map(task, asyncStatus); } } return asyncStatus; } private CreationStatus getJobIdStatus(VdcReturnValueBase result) { Guid jobId = result.getJobId(); if (jobId == null || jobId.equals(Guid.Empty)) { return CreationStatus.COMPLETE; } else { IdQueryParameters params = new IdQueryParameters(jobId); VdcQueryReturnValue queryResult = runQuery(VdcQueryType.GetJobByJobId, params); if (queryResult != null && queryResult.getSucceeded() && queryResult.getReturnValue() != null) { Job job = queryResult.getReturnValue(); return job.getStatus()==JobExecutionStatus.STARTED ? CreationStatus.IN_PROGRESS : CreationStatus.COMPLETE; } else { //not supposed to happen return CreationStatus.COMPLETE; } } } protected boolean incomplete(CreationStatus status) { return status == null || status == CreationStatus.PENDING || status == CreationStatus.IN_PROGRESS; } protected void delay(long delay) { try { Thread.sleep(delay); } catch (InterruptedException ie) { // ignore } } /** * Populates the entity with additional information, which is not returned by the main backend query. Checks for * "All-Content=true" header before populating. Population logic itself is implemented in doPopulate(). */ protected final R populate(R model, Q entity) { model = deprecatedPopulate(model, entity); return isPopulate() ? doPopulate(model, entity) : model; } @Deprecated protected R deprecatedPopulate(R model, Q entity) { return model; } /** * Populates the entity with additional information, which is not returned by the main backend query. Override this * method to add population logic to an entity. */ protected R doPopulate(R model, Q entity) { return model; } /** * Add any parent resource references needed for constructing links. * * LinkHelper.addLinks() constructs the 'href' attribute from @model * using its 'id' attribute and the 'id' attribute of any parent * resources. * * This method provides the hook through which all sub-resource * classes should add references to parent resources so that * LinkHelper can do its job. * e.g. in order to get a URL like 'clusters/{cid}/networks/{nid}' * you would need to have: * * protected Network addParents(Network network) { * network.setCluster(new Cluster()); * network.getCluster().setId(clusterId); * return network; * } * * @param model the resource representation * @return the model with any parent references added */ protected R addParents(R model) { return model; } protected R addLinks(R model, String... subCollectionMembersToExclude) { return addLinks(model, null, subCollectionMembersToExclude); } protected R addLinks(R model, boolean doNotLinkSubCollections) { return addLinks(model, null, doNotLinkSubCollections); } protected R addLinks(R model, Class<? extends BaseResource> suggestedParent, String... subCollectionMembersToExclude) { model = addParents(model); // linkSubCollections called first as addLinks unsets the grandparent model model = linkSubCollections(model, suggestedParent, subCollectionMembersToExclude); model = LinkHelper.addLinks(model, suggestedParent); return model; } protected R addLinks(R model, Class<? extends BaseResource> suggestedParent, boolean doNotLinkSubCollections) { return doNotLinkSubCollections? LinkHelper.addLinks(addParents(model), suggestedParent) : addLinks(model, suggestedParent); } protected List<Q> asCollection(Object o) { return asCollection(entityType, o); } protected String asString(List<Guid> list) { StringBuilder builder = new StringBuilder(); for (Guid id : list) { if (builder.length() > 0) { builder.append(urlEncode(ID_SEPARATOR)); } builder.append(id.toString()); } return builder.toString(); } protected String urlEncode(String url) { try { return URLEncoder.encode(url, "UTF-8"); } catch (UnsupportedEncodingException e) { // UTF-8 should always be supported e.printStackTrace(); return null; } } protected R newModel(String id) { R ret = null; try { ret = modelType.newInstance(); ret.setId(id); ret = addParents(ret); } catch (Exception e) { // trivial construction, should not fail } return ret; } protected R linkSubResource(R model, String subResource, String oid) { String path = LinkHelper.getPath(model); if (path != null) { String href = String.join("/", path, subResource, oid); addOrUpdateLink(model, subResource, href); } return model; } protected R linkSubCollections(R model, Class<? extends BaseResource> suggestedParent, String... subCollectionMembersToExclude) { if (subCollections != null) { String path = LinkHelper.getPath(model, suggestedParent); for (String relation : subCollections) { if(!shouldExclude(relation, subCollectionMembersToExclude)) { if (path != null) { String href = String.join("/", path, relation); addOrUpdateLink(model, relation, href); } } else { removeIfExist(model, relation); } } } return model; } private boolean shouldExclude(String member, String[] subCollectionMembersToExclude) { if(subCollectionMembersToExclude !=null && subCollectionMembersToExclude.length > 0){ for(String excludeMember : subCollectionMembersToExclude){ if (member.equals(excludeMember)) { return true; } } } return false; } protected R injectSearchLinks(R resource, String[] rels){ for(String rel : rels){ resource.getLinks().add(LinkCreator.createSearchLink(resource.getHref(), rel)); } return resource; } protected <B extends BaseResource >void removeIfExist(B model, String relation) { List<Link> linksCopy = new ArrayList<>(model.getLinks()); for (Link link : model.getLinks()) { if (link.getRel().equals(relation)) { linksCopy.remove(link); break; } } model.getLinks().retainAll(linksCopy); } protected <B extends BaseResource >void addOrUpdateLink(B model, String relation, String href) { for (Link link : model.getLinks()) { if (link.getRel().equals(relation)) { link.setHref(href); return; } } Link link = new Link(); link.setRel(relation); link.setHref(href); model.getLinks().add(link); } protected <T> VdcQueryParametersBase getQueryParams(Class<? extends VdcQueryParametersBase> queryParamsClass, T id) { VdcQueryParametersBase params = null; try { params = queryParamsClass.getConstructor(id.getClass()).newInstance(id); } catch (Exception e) { // trivial class construction } return params; } /** * Convert a string to a Guid, or return a 404 response. * * If an invalid UUID is supplied to a sub-resource locator, this * method will cause us to return a 404 response via the sub-resource * constructor. * * @param id the incoming UUID * @return a Guid * @throws WebApplicationException a 404 response, if the UUID is invalid */ protected Guid asGuidOr404(String id) { try { return asGuid(id); } catch (IllegalArgumentException iae) { throw new WebApplicationException(Response.Status.NOT_FOUND); } } protected R notFound() { return notFound(modelType); } protected Q entityNotFound() { return notFound(entityType); } /** * This method will raise an WebFaultException with message describing the entity was not found. * * @param name The name of the entity */ protected void notFound(String name) { throw new WebFaultException( null, localize(Messages.BACKEND_FAILED), localize(Messages.ENTITY_NOT_FOUND_TEMPLATE, name), Response.Status.NOT_FOUND ); } protected <T> T notFound(Class<T> clz) { throw new WebApplicationException(Response.Status.NOT_FOUND); } protected Guid getHostId(Host host) { return host.isSetId() ? asGuid(host.getId()) : getEntity(VDS.class, VdcQueryType.GetVdsByName, new NameQueryParameters(host.getName()), host.getName()).getId(); } protected abstract class EntityIdResolver<T> implements IResolver<T, Q> { public abstract Q lookupEntity(T id) throws BackendFailureException; @Override public Q resolve(T id) throws BackendFailureException { Q entity = lookupEntity(id); if (entity == null) { throw new EntityNotFoundException(id.toString()); } return entity; } } protected abstract class EntityResolver<T> implements IResolver<T, Q> { public abstract Q lookupEntity(T id) throws BackendFailureException; @Override public Q resolve(T id) throws BackendFailureException { Q entity = lookupEntity(id); if (entity == null) { throw new EntityNotFoundException(id.toString()); } return entity; } } public class QueryIdResolver<T> extends EntityIdResolver<T> { private final VdcQueryType query; private final Class<? extends VdcQueryParametersBase> queryParamsClass; public QueryIdResolver(VdcQueryType query, Class<? extends VdcQueryParametersBase> queryParamsClass) { this.query = query; this.queryParamsClass = queryParamsClass; } @Override public Q lookupEntity(T id) throws BackendFailureException { return doGetEntity(entityType, query, getQueryParams(queryParamsClass, id), id.toString()); } } /** * Returns an array with the names of subcollections of this resource, * e.g, for BackendVmResource: ["affinitylabels", "applications", "cdroms", * "diskattachments", "graphicsconsoles", "hostdevices", "katelloerrata"...] */ protected String[] getSubCollections() { Set<Class<?>> interfaces = getInterfaces(); List<String> subCollections = new ArrayList<>(); for (Class<?> clazz : interfaces) { ServiceTreeNode node = getRequiredNode(clazz); if (node != null) { addSubCollections(node, subCollections); } } return subCollections.toArray(new String[0]); } /** * Returns the set of service interfaces implemented by this resource. This needs to make a recursive search in * order to find the interfaces implemented directly and also the interfaces implemented by base classes. */ private Set<Class<?>> getInterfaces() { Set<Class<?>> result = new HashSet<>(); for (Class<?> clazz = getClass(); clazz != null; clazz = clazz.getSuperclass()) { for (Class<?> iface : clazz.getInterfaces()) { if (iface.getName().endsWith("Resource")) { result.add(iface); } } } return result; } /** * Get the node in the API ServiceTree which contains sub-collections info. * For 'collection' services (such as VmsService) the sub-collections info * is held in the 'son' node (VmService). So for both the collection and * 'single' service, the node of the 'single' service is returned. */ private ServiceTreeNode getRequiredNode(Class<?> clazz) { ServiceTreeNode node = ServiceTree.getNode(clazz); return node==null ? null : node.getSon()!=null ? node.getSon() : node; } private static void addSubCollections(ServiceTreeNode node, List<String> subCollections) { for (ServiceTreeNode innerNode : node.getSubServices()) { subCollections.add(innerNode.getPath()); } } }