package io.cattle.platform.api.resource.jooq; import io.cattle.platform.api.auth.Policy; import io.cattle.platform.api.resource.AbstractObjectResourceManager; import io.cattle.platform.api.utils.ApiUtils; import io.cattle.platform.engine.process.ExitReason; import io.cattle.platform.engine.process.ProcessInstanceException; import io.cattle.platform.engine.process.impl.ProcessCancelException; import io.cattle.platform.engine.process.impl.ProcessExecutionExitException; import io.cattle.platform.lock.exception.FailedToAcquireLockException; import io.cattle.platform.object.jooq.utils.JooqUtils; import io.cattle.platform.object.meta.MapRelationship; import io.cattle.platform.object.meta.ObjectMetaDataManager; import io.cattle.platform.object.meta.Relationship; import io.cattle.platform.object.meta.Relationship.RelationshipType; import io.cattle.platform.util.exception.ExceptionUtils; import io.github.ibuildthecloud.gdapi.context.ApiContext; import io.github.ibuildthecloud.gdapi.exception.ClientVisibleException; import io.github.ibuildthecloud.gdapi.factory.SchemaFactory; import io.github.ibuildthecloud.gdapi.model.Include; import io.github.ibuildthecloud.gdapi.model.ListOptions; import io.github.ibuildthecloud.gdapi.model.Pagination; import io.github.ibuildthecloud.gdapi.model.Schema; import io.github.ibuildthecloud.gdapi.model.Sort; import io.github.ibuildthecloud.gdapi.request.ApiRequest; import io.github.ibuildthecloud.gdapi.util.ResponseCodes; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import javax.inject.Inject; import org.jooq.Condition; import org.jooq.Configuration; import org.jooq.DSLContext; import org.jooq.JoinType; import org.jooq.SelectQuery; import org.jooq.Table; import org.jooq.TableField; import org.jooq.exception.DataAccessException; import org.jooq.impl.DSL; import org.jooq.impl.DefaultDSLContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public abstract class AbstractJooqResourceManager extends AbstractObjectResourceManager { private static final Logger log = LoggerFactory.getLogger(AbstractJooqResourceManager.class); Configuration configuration; protected DSLContext create() { return new DefaultDSLContext(configuration); } @Override protected Object listInternal(SchemaFactory schemaFactory, String type, Map<Object, Object> criteria, ListOptions options) { return listInternal(schemaFactory, type, criteria, options, null); } protected Object listInternal(SchemaFactory schemaFactory, String type, Map<Object, Object> criteria, ListOptions options, Map<Table<?>, Condition> joins) { Class<?> clz = getClass(schemaFactory, type, criteria, true); if (clz == null) { return null; } /* Use core schema, parent may not be authorized */ type = getObjectManager().getSchemaFactory().getSchemaName(clz); Table<?> table = JooqUtils.getTableFromRecordClass(clz); Sort sort = options == null ? null : options.getSort(); Pagination pagination = options == null ? null : options.getPagination(); Include include = options == null ? null : options.getInclude(); if (table == null) return null; SelectQuery<?> query = create().selectQuery(); addSort(schemaFactory, type, sort, query); MultiTableMapper mapper = addTables(schemaFactory, query, type, table, criteria, include, pagination, joins); addJoins(query, joins); addConditions(schemaFactory, query, type, table, criteria); addLimit(schemaFactory, type, pagination, query); List<?> result = mapper == null ? query.fetch() : query.fetchInto(mapper); processPaginationResult(result, pagination, mapper); return result; } protected void addJoins(SelectQuery<?> query, Map<Table<?>, Condition> joins) { if (joins == null) { return; } for (Map.Entry<Table<?>, Condition> entry : joins.entrySet()) { query.addJoin(entry.getKey(), JoinType.LEFT_OUTER_JOIN, entry.getValue()); } } protected void processPaginationResult(List<?> result, Pagination pagination, MultiTableMapper mapper) { Integer limit = pagination == null ? null : pagination.getLimit(); if (limit == null) { return; } long offset = getOffset(pagination); boolean partial = false; if (mapper == null) { partial = result.size() > limit; if (partial) { result.remove(result.size() - 1); } } else { partial = mapper.getResultSize() > limit; } if (partial) { Pagination paginationResponse = new Pagination(limit); paginationResponse.setPartial(true); paginationResponse.setNext(ApiContext.getUrlBuilder().next("m" + (offset + limit))); pagination.setResponse(paginationResponse); } else { pagination.setResponse(new Pagination(limit)); } } protected int getOffset(Pagination pagination) { Object marker = getMarker(pagination); if (marker == null) { return 0; } else if (marker instanceof String) { /* * Important to check that marker is a string. If you don't then * somebody could use the marker functionality to deobfuscate ID's * and find their long value. */ try { return Integer.parseInt((String) marker); } catch (NumberFormatException nfe) { return 0; } } return 0; } protected Class<?> getClass(SchemaFactory schemaFactory, String type, Map<Object, Object> criteria, boolean alterCriteria) { Schema schema = schemaFactory.getSchema(type); Class<?> clz = schemaFactory.getSchemaClass(type); Schema clzSchema = schemaFactory.getSchema(clz); if (clz != null && (clzSchema == null || !schema.getId().equals(clzSchema.getId())) && alterCriteria) { criteria.put(ObjectMetaDataManager.KIND_FIELD, type); } return clz; } protected MultiTableMapper addTables(SchemaFactory schemaFactory, SelectQuery<?> query, String type, Table<?> table, Map<Object, Object> criteria, Include include, Pagination pagination, Map<Table<?>, Condition> joins) { if ((joins == null || joins.size() == 0) && (include == null || include.getLinks().size() == 0)) { query.addFrom(table); return null; } MultiTableMapper tableMapper = new MultiTableMapper(getMetaDataManager(), pagination); tableMapper.map(table); if (include == null) { query.addSelect(tableMapper.getFields()); query.addFrom(table); return tableMapper; } List<Relationship> rels = new ArrayList<Relationship>(); rels.add(null); for (Map.Entry<String, Relationship> entry : getLinkRelationships(schemaFactory, type, include).entrySet()) { Relationship rel = entry.getValue(); Table<?> childTable = JooqUtils.getTableFromRecordClass(rel.getObjectType()); if (childTable == null) { throw new IllegalStateException("Failed to find table for type [" + rel.getObjectType() + "]"); } else { String key = rel.getRelationshipType() == RelationshipType.REFERENCE ? ApiUtils.SINGLE_ATTACHMENT_PREFIX + rel.getName() : rel.getName(); tableMapper.map(key, childTable); rels.add(rel); } } List<Table<?>> tables = tableMapper.getTables(); query.addSelect(tableMapper.getFields()); query.addFrom(table); for (int i = 0; i < tables.size(); i++) { Relationship rel = rels.get(i); Table<?> toTable = tables.get(i); if (rel != null) { if (rel.getRelationshipType() == RelationshipType.MAP) { addMappingJoins(query, toTable, schemaFactory, type, table, toTable.getName(), (MapRelationship) rel); } else { addJoin(query, toTable, schemaFactory, type, table, toTable.getName(), rel); } } } return tableMapper; } protected void addMappingJoins(SelectQuery<?> query, Table<?> toTable, SchemaFactory schemaFactory, String fromType, Table<?> from, String asName, MapRelationship rel) { Table<?> mappingTable = JooqUtils.getTableFromRecordClass(rel.getMappingType()); /* * We don't required the mapping type to be visible external, that's why * we use the schemaFactory from the objectManager, because it is the * superset schemaFactory. */ String mappingType = getObjectManager().getSchemaFactory().getSchemaName(rel.getMappingType()); TableField<?, Object> fieldFrom = JooqUtils.getTableField(getMetaDataManager(), fromType, ObjectMetaDataManager.ID_FIELD); TableField<?, Object> fieldTo = JooqUtils.getTableField(getMetaDataManager(), mappingType, rel.getPropertyName()); TableField<?, Object> fieldRemoved = JooqUtils.getTableField(getMetaDataManager(), mappingType, ObjectMetaDataManager.REMOVED_FIELD); org.jooq.Condition cond = fieldFrom.eq(fieldTo.getTable().field(fieldTo.getName())).and( fieldRemoved == null ? DSL.trueCondition() : fieldRemoved.isNull()); query.addJoin(mappingTable, JoinType.LEFT_OUTER_JOIN, cond); fieldFrom = JooqUtils.getTableField(getMetaDataManager(), mappingType, rel.getOtherRelationship().getPropertyName()); fieldTo = JooqUtils.getTableField(getMetaDataManager(), schemaFactory.getSchemaName(rel.getObjectType()), ObjectMetaDataManager.ID_FIELD); cond = fieldFrom.eq(fieldTo.getTable().asTable(asName).field(fieldTo.getName())); query.addJoin(toTable, JoinType.LEFT_OUTER_JOIN, cond); query.addOrderBy(fieldTo.getTable().asTable(asName).field(fieldTo.getName()).asc()); } protected void addJoin(SelectQuery<?> query, Table<?> toTable, SchemaFactory schemaFactory, String fromType, Table<?> from, String asName, Relationship rel) { TableField<?, Object> fieldFrom = null; TableField<?, Object> fieldTo = null; switch (rel.getRelationshipType()) { case REFERENCE: fieldFrom = JooqUtils.getTableField(getMetaDataManager(), fromType, rel.getPropertyName()); fieldTo = JooqUtils.getTableField(getMetaDataManager(), schemaFactory.getSchemaName(rel.getObjectType()), ObjectMetaDataManager.ID_FIELD); break; case CHILD: fieldFrom = JooqUtils.getTableField(getMetaDataManager(), fromType, ObjectMetaDataManager.ID_FIELD); fieldTo = JooqUtils.getTableField(getMetaDataManager(), schemaFactory.getSchemaName(rel.getObjectType()), rel.getPropertyName()); break; default: throw new IllegalArgumentException("Illegal Relationship type [" + rel.getRelationshipType() + "]"); } if (fieldFrom == null || fieldTo == null) { throw new IllegalStateException("Failed to construction join query for [" + fromType + "] [" + from + "] [" + rel + "]"); } query.addJoin(toTable, JoinType.LEFT_OUTER_JOIN, fieldFrom.eq(fieldTo.getTable().as(asName).field(fieldTo.getName()))); query.addOrderBy(fieldTo.getTable().as(asName).field(ObjectMetaDataManager.ID_FIELD).asc()); } protected void addConditions(SchemaFactory schemaFactory, SelectQuery<?> query, String type, Table<?> table, Map<Object, Object> criteria) { org.jooq.Condition condition = JooqUtils.toConditions(getMetaDataManager(), type, criteria); if (condition != null) { query.addConditions(condition); } } @Override protected Object getMapLink(String fromType, String id, MapRelationship rel, ApiRequest request) { SchemaFactory schemaFactory = request.getSchemaFactory(); /* * We don't required the mapping type to be visible external, that's why * we use the schemaFactory from the objectManager, because it is the * superset schemaFactory. */ String mappingType = getObjectManager().getSchemaFactory().getSchemaName(rel.getMappingType()); String type = schemaFactory.getSchemaName(rel.getObjectType()); Map<Table<?>, Condition> joins = new LinkedHashMap<Table<?>, Condition>(); Map<Object, Object> criteria = new LinkedHashMap<Object, Object>(); if (mappingType == null || type == null) { return null; } Table<?> mappingTable = JooqUtils.getTable(schemaFactory, rel.getMappingType()); TableField<?, Object> fieldFrom = JooqUtils.getTableField(getMetaDataManager(), type, ObjectMetaDataManager.ID_FIELD); TableField<?, Object> fieldTo = JooqUtils.getTableField(getMetaDataManager(), mappingType, rel.getOtherRelationship().getPropertyName()); TableField<?, Object> fieldRemoved = JooqUtils.getTableField(getMetaDataManager(), mappingType, ObjectMetaDataManager.REMOVED_FIELD); TableField<?, Object> fromTypeIdField = JooqUtils.getTableField(getMetaDataManager(), mappingType, rel.getSelfRelationship().getPropertyName()); org.jooq.Condition cond = fieldFrom.eq(fieldTo.getTable().field(fieldTo.getName())).and( fieldRemoved == null ? DSL.trueCondition() : fieldRemoved.isNull()); joins.put(mappingTable, cond); criteria.put(Condition.class, fromTypeIdField.eq(id)); return listInternal(schemaFactory, type, criteria, new ListOptions(request), joins); } protected void addLimit(SchemaFactory schemaFactory, String type, Pagination pagination, SelectQuery<?> query) { if (pagination == null || pagination.getLimit() == null) { return; } int limit = pagination.getLimit() + 1; int offset = getOffset(pagination); query.addLimit(offset, limit); } protected void addSort(SchemaFactory schemaFactory, String type, Sort sort, SelectQuery<?> query) { if (sort != null) { TableField<?, Object> sortField = JooqUtils.getTableField(getMetaDataManager(), type, sort.getName()); if (sortField == null) { return; } switch (sort.getOrderEnum()) { case DESC: query.addOrderBy(sortField.desc()); break; default: query.addOrderBy(sortField.asc()); } } TableField<?, Object> idSort = JooqUtils.getTableField(getMetaDataManager(), type, ObjectMetaDataManager.ID_FIELD); if (idSort == null) { return; } if (sort != null) { switch (sort.getOrderEnum()) { case DESC: query.addOrderBy(idSort.desc()); break; default: query.addOrderBy(idSort.asc()); } } else { query.addOrderBy(idSort.asc()); } } @Override protected void addAccountAuthorization(boolean byId, boolean byLink, String type, Map<Object, Object> criteria, Policy policy) { super.addAccountAuthorization(byId, byLink, type, criteria, policy); if (!policy.isOption(Policy.LIST_ALL_ACCOUNTS)) { if (policy.isOption(Policy.AUTHORIZED_FOR_ALL_ACCOUNTS) && (byId || byLink)) { return; } TableField<?, Object> accountField = JooqUtils.getTableField(getMetaDataManager(), type, ObjectMetaDataManager.ACCOUNT_FIELD); TableField<?, Object> publicField = JooqUtils.getTableField(getMetaDataManager(), type, ObjectMetaDataManager.PUBLIC_FIELD); Object accountValue = criteria.get(ObjectMetaDataManager.ACCOUNT_FIELD); if (accountField == null || publicField == null || accountValue == null) { return; } ApiRequest request = ApiContext.getContext().getApiRequest(); // Only allow is_public logic for GET methods if (request == null) { return; } if ("GET".equals(request.getMethod()) || ("POST".equals(request.getMethod()) && request.getAction() == null)) { criteria.remove(ObjectMetaDataManager.ACCOUNT_FIELD); Condition accountCondition = null; if (accountValue instanceof io.github.ibuildthecloud.gdapi.condition.Condition) { accountCondition = accountField.in(((io.github.ibuildthecloud.gdapi.condition.Condition) accountValue).getValues()); } else { accountCondition = accountField.eq(accountValue); } criteria.put(Condition.class, publicField.isTrue().or(accountCondition)); } } } @Override protected Object removeFromStore(String type, String id, Object obj, ApiRequest request) { Table<?> table = JooqUtils.getTableFromRecordClass(JooqUtils.getRecordClass(request.getSchemaFactory(), obj.getClass())); TableField<?, Object> idField = JooqUtils.getTableField(getMetaDataManager(), type, ObjectMetaDataManager.ID_FIELD); int result = create().delete(table).where(idField.eq(id)).execute(); if (result != 1) { log.error("While deleting type [{}] and id [{}] got a result of [{}]", type, id, result); throw new ClientVisibleException(ResponseCodes.CONFLICT); } return obj; } @Override public boolean handleException(Throwable t, ApiRequest apiRequest) { if (t instanceof ProcessInstanceException) { t = ExceptionUtils.getRootCause(t); } if (t instanceof ProcessExecutionExitException && ((ProcessExecutionExitException) t).getExitReason() == ExitReason.RESOURCE_BUSY) { log.info("Resource busy : {}", t.getMessage()); throw new ClientVisibleException(ResponseCodes.CONFLICT); } else if (t instanceof ProcessExecutionExitException && ((ProcessExecutionExitException) t).getExitReason() == ExitReason.PROCESS_ALREADY_IN_PROGRESS) { log.info("Process in progress : {}", t.getMessage()); throw new ClientVisibleException(ResponseCodes.CONFLICT); } else if (t instanceof ProcessExecutionExitException && ((ProcessExecutionExitException) t).getExitReason() == ExitReason.STATE_CHANGED) { log.info("State changed: {}", t.getMessage()); throw new ClientVisibleException(ResponseCodes.CONFLICT); } else if (t instanceof FailedToAcquireLockException) { log.info("Failed to lock : {}", t.getMessage()); throw new ClientVisibleException(ResponseCodes.CONFLICT); } else if (t instanceof ProcessCancelException) { log.info("Process cancel : {}", t.getMessage()); throw new ClientVisibleException(ResponseCodes.CONFLICT); } else if (t instanceof DataAccessException) { log.info("Database error : {}", t.getMessage()); throw new ClientVisibleException(ResponseCodes.CONFLICT); } return super.handleException(t, apiRequest); } public Configuration getConfiguration() { return configuration; } @Inject public void setConfiguration(Configuration configuration) { this.configuration = configuration; } }