Skip to content

Commit

Permalink
Merge branch 'main' into issue-30521-Create-content-analytics-query-e…
Browse files Browse the repository at this point in the history
…ndpoint-that-doesnt-require-map
  • Loading branch information
jcastro-dotcms authored Dec 5, 2024
2 parents 16b0bac + 1a6672f commit 82151de
Show file tree
Hide file tree
Showing 12 changed files with 938 additions and 92 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,34 @@
import com.dotcms.rest.ResponseEntityView;
import com.dotcms.rest.WebResource;
import com.dotcms.rest.annotation.NoCache;
import com.dotcms.util.filtering.Specification;
import com.dotmarketing.business.APILocator;
import com.dotmarketing.exception.DotDataException;
import com.dotmarketing.exception.DotSecurityException;
import com.dotmarketing.util.Logger;
import com.liferay.portal.model.User;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import org.glassfish.jersey.server.JSONP;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
Expand All @@ -36,6 +45,7 @@
*/
@Path("/v3/contenttype/{typeIdOrVarName}/fields")
public class FieldResource {

private final WebResource webResource;
private final ContentTypeFieldLayoutAPI contentTypeFieldLayoutAPI;

Expand Down Expand Up @@ -233,4 +243,122 @@ public Response deleteFields(
)
)).build();
}

/**
* Returns the list of fields in a Content Type that meet the specified criteria. For instance,
* if you need to retrieve all fields marked as 'required', 'unique'`, and 'system indexed',
* you can call the endpoint like this:
* <pre>
* {@code
* {{serverURL}}/api/v3/contenttype/AAA/fields/allfields?filter=REQUIRED&filter=SYSTEM_INDEXED&filter=UNIQUE
* }
* </pre>
* Just add the same `filter` parameter for each criterion you want to apply.
*
* @param request The current instance of the {@link HttpServletRequest}.
* @param response The current instance of the {@link HttpServletResponse}.
* @param typeIdOrVarName The Identifier or Velocity Variable Name of a given
* {@link ContentType}.
* @param criteria A set of {@link FilteringCriteria} objects that will be used to filter
* the fields.
*
* @return A {@link FieldResponseView} object that contains the list of {@link Field} objects
* that meet the specified criteria.
*
* @throws DotDataException An error occurred when interacting with the database.
* @throws DotSecurityException The specified User does not have the necessary permissions to
* execute this operation.
*/
@GET
@Path("/allfields")
@JSONP
@NoCache
@Consumes(MediaType.APPLICATION_JSON)
@Produces({MediaType.APPLICATION_JSON, "application/javascript"})
@Operation(
operationId = "allfields",
summary = "Returns filtered Content Type fields",
description = "Returns the list of fields in a Content Type that meet the specified criteria.",
tags = {"Content Type Field"},
requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
content = @Content(
examples = {
@ExampleObject(
name = "filter",
value = "REQUIRED,SYSTEM_INDEXED,UNIQUE,SHOW_IN_LIST,USER_SEARCHABLE",
summary = "Filter fields by one or more of the specified criteria"
)
}
)
),
responses = {
@ApiResponse(responseCode = "200", description = "Content type retrieved successfully",
content = @Content(mediaType = "application/json",
examples = {
@ExampleObject(
description = "Returning a list of one Field matching the filtering criteria.",
value = "{\n" +
" \"entity\": [\n" +
" {\n" +
" \"clazz\": \"com.dotcms.contenttype.model.field.ImmutableTextField\",\n" +
" \"contentTypeId\": \"3b70f386cf65117a675f284eea928415\",\n" +
" \"dataType\": \"TEXT\",\n" +
" \"dbColumn\": \"text2\",\n" +
" \"defaultValue\": null,\n" +
" \"fixed\": false,\n" +
" \"forceIncludeInApi\": false,\n" +
" \"hint\": null,\n" +
" \"iDate\": 1732992811000,\n" +
" \"id\": \"e39533a92ee05d8c083f7e6a1a5ee5e5\",\n" +
" \"indexed\": true,\n" +
" \"listed\": false,\n" +
" \"modDate\": 1732992838000,\n" +
" \"name\": \"Description\",\n" +
" \"owner\": null,\n" +
" \"readOnly\": false,\n" +
" \"regexCheck\": null,\n" +
" \"relationType\": null,\n" +
" \"required\": true,\n" +
" \"searchable\": false,\n" +
" \"sortOrder\": 3,\n" +
" \"unique\": true,\n" +
" \"values\": null,\n" +
" \"variable\": \"description\"\n" +
" }\n" +
" ],\n" +
" \"errors\": [],\n" +
" \"i18nMessagesMap\": {},\n" +
" \"messages\": [],\n" +
" \"pagination\": null,\n" +
" \"permissions\": []\n" +
"}"
)
}
)
),
@ApiResponse(responseCode = "400", description = "Bad Request, when using invalid filter names"),
@ApiResponse(responseCode = "401", description = "Invalid User"),
@ApiResponse(responseCode = "404", description = "Content Type was not found"),
@ApiResponse(responseCode = "500", description = "Internal Server Error")
}
)
public FieldResponseView allFieldsBy(@Context final HttpServletRequest request,
@Context final HttpServletResponse response,
@PathParam("typeIdOrVarName") final String typeIdOrVarName,
@QueryParam("filter") final Set<FilteringCriteria> criteria) throws DotDataException, DotSecurityException {
final InitDataObject initDataObject = new WebResource.InitBuilder(this.webResource)
.requestAndResponse(request, response)
.requiredBackendUser(true)
.rejectWhenNoUser(true)
.init();
final User user = initDataObject.getUser();
Logger.debug(this, () -> String.format("Returning filtered fields from Content Type '%s' " +
"using the criteria: %s", typeIdOrVarName, criteria));
final ContentType contentType = APILocator.getContentTypeAPI(user).find(typeIdOrVarName);
final Specification<Field> fieldSpecification = FilteringCriteria.specificationsFrom(criteria);
final List<Field> filteredFields = contentType.fields().stream().filter(fieldSpecification::isSatisfiedBy)
.collect(Collectors.toList());
return new FieldResponseView(filteredFields);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.dotcms.rest.api.v3.contenttype;

import com.dotcms.contenttype.model.field.Field;
import com.dotcms.rest.ResponseEntityView;

import java.util.List;

/**
* This class represents a Response View for objects of type {@link Field}. It's very useful for
* returning a list of fields, or a filtered sub-set if necessary.
*
* @author Jose Castro
* @since Nov 29th, 2024
*/
public class FieldResponseView extends ResponseEntityView<List<Field>> {

public FieldResponseView(final List<Field> entity) {
super(entity);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.dotcms.rest.api.v3.contenttype;

import com.dotcms.contenttype.model.field.Field;
import com.dotcms.util.filtering.Specification;
import com.dotcms.util.filtering.contenttype.field.CombinedSpecification;
import com.dotcms.util.filtering.contenttype.field.RequiredSpecification;
import com.dotcms.util.filtering.contenttype.field.ShowInListSpecification;
import com.dotcms.util.filtering.contenttype.field.SystemIndexedSpecification;
import com.dotcms.util.filtering.contenttype.field.UniqueSpecification;
import com.dotcms.util.filtering.contenttype.field.UserSearchableSpecification;

import java.util.Set;

/**
* Enum that represents the different criteria that can be used to filter fields in a Content Type.
* This allows you to get a specific list of {@link Field}s from a Content Type that meet your
* specified criteria via @{link Specification} classes. In here, you can define as many
* Specifications as you need.
* <p>The initial specifications represent the default attributes that you can enable for a given
* field, such as:</p>
* <ul>
* <li>Required.</li>
* <li>User Searchable.</li>
* <li>System Indexed.</li>
* <li>Show in List.</li>
* <li>Unique.</li>
* </ul>
*
* @author Jose Castro
* @since Nov 30th, 2024
*/
public enum FilteringCriteria {

REQUIRED(new RequiredSpecification()),
USER_SEARCHABLE(new UserSearchableSpecification()),
SYSTEM_INDEXED(new SystemIndexedSpecification()),
SHOW_IN_LIST(new ShowInListSpecification()),
UNIQUE(new UniqueSpecification());

final Specification<Field> specification;

FilteringCriteria(final Specification<Field> specification) {
this.specification = specification;
}

public Specification<Field> specification() {
return this.specification;
}

/**
* Takes the list of criteria that will be used to query for specific fields in a Content Type
* and generates the appropriate Specification object to be used in the filtering.
*
* @param criteria A Set of FilteringCriteria objects that will be used to filter the fields.
*
* @return The {@link Specification} object that will be used to filter the fields.
*/
public static Specification<Field> specificationsFrom(final Set<FilteringCriteria> criteria) {
Specification<Field> mainSpec = new CombinedSpecification();
for (final FilteringCriteria criterion : criteria) {
mainSpec = mainSpec.and(criterion.specification());
}
return mainSpec;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
@Tag(name = "Workflow"),
@Tag(name = "Page"),
@Tag(name = "Content Type"),
@Tag(name = "Content Type Field"),
@Tag(name = "Content Delivery"),
@Tag(name = "Bundle"),
@Tag(name = "Navigation"),
Expand Down
61 changes: 61 additions & 0 deletions dotCMS/src/main/java/com/dotcms/util/filtering/Specification.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.dotcms.util.filtering;

import java.io.Serializable;

/**
* This class represents the base Specification interface for filtering objects that meet given
* satisfaction criteria using the <b>Specification Pattern</b>, which is a behavioral pattern
* often used to encapsulate filtering logic.
* <p>By implementing this interface, you can create your own custom Specification class to filter
* objects based on given criteria.</p>
*
* @param <T> The type of the object to be filtered.
*
* @author Jose Castro
* @since Nov 29th, 2024
*/
public interface Specification<T> extends Serializable {

/**
* Determines whether the provided object meets the satisfaction criteria for your Specification
* or not.
*
* @param item The object to be evaluated.
*
* @return If the object meets your criteria, returns {@code true}.
*/
boolean isSatisfiedBy(final T item);

/**
* Allows you to chain multiple Specifications together using the logical AND operator.
*
* @param other The other Specification to be chained.
*
* @return If all chained Specifications are satisfied, returns {@code true}.
*/
default Specification<T> and(final Specification<T> other) {
return item -> Specification.this.isSatisfiedBy(item) && other.isSatisfiedBy(item);
}

/**
* Allows you to chain multiple Specifications together using the logical OR operator.
*
* @param other The other Specification to be chained.
*
* @return If any of the chained Specifications are satisfied, returns {@code true}.
*/
default Specification<T> or(final Specification<T> other) {
return item -> this.isSatisfiedBy(item) || other.isSatisfiedBy(item);
}

/**
* Allows you to chain multiple Specifications together using the logical NOT operator. This is
* particularly useful for excluding objects from your result set.
*
* @return If the Specification is not satisfied, returns {@code true}.
*/
default Specification<T> not() {
return item -> !this.isSatisfiedBy(item);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.dotcms.util.filtering.contenttype.field;

import com.dotcms.contenttype.model.field.Field;
import com.dotcms.util.filtering.Specification;

/**
* This simple Specification serves as a base/placeholder for developers to combine multiple
* {@link Specification} objects. This is very useful when the given list of Specifications is
* dynamic.
*
* @author Jose Castro
* @since Nov 29th, 2024
*/
public class CombinedSpecification implements Specification<Field> {

@Override
public boolean isSatisfiedBy(final Field field) {
// This base Specification returns 'true' by default
return true;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.dotcms.util.filtering.contenttype.field;

import com.dotcms.contenttype.model.field.Field;
import com.dotcms.util.filtering.Specification;

/**
* This Specification is responsible for verifying if a Field is flagged as {@code Required}.
*
* @author Jose Castro
* @since Nov 29th, 2024
*/
public class RequiredSpecification implements Specification<Field> {

@Override
public boolean isSatisfiedBy(final Field field) {
return field.required();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.dotcms.util.filtering.contenttype.field;

import com.dotcms.contenttype.model.field.Field;
import com.dotcms.util.filtering.Specification;

/**
* This Specification is responsible for verifying if a Field is flagged as {@code Show in List}.
*
* @author Jose Castro
* @since Nov 29th, 2024
*/
public class ShowInListSpecification implements Specification<Field> {

@Override
public boolean isSatisfiedBy(final Field field) {
return field.listed();
}

}
Loading

0 comments on commit 82151de

Please sign in to comment.