diff --git a/pantheon-bundle/src/main/java/com/redhat/pantheon/servlet/MapConverters.java b/pantheon-bundle/src/main/java/com/redhat/pantheon/servlet/MapConverters.java new file mode 100644 index 000000000..cdb2b5a09 --- /dev/null +++ b/pantheon-bundle/src/main/java/com/redhat/pantheon/servlet/MapConverters.java @@ -0,0 +1,154 @@ +package com.redhat.pantheon.servlet; + +import com.google.common.base.Charsets; +import com.redhat.pantheon.extension.url.CustomerPortalUrlUuidProvider; +import com.redhat.pantheon.html.Html; +import com.redhat.pantheon.model.ProductVersion; +import com.redhat.pantheon.model.api.FileResource; +import com.redhat.pantheon.model.module.ModuleMetadata; +import com.redhat.pantheon.model.module.ModuleVariant; +import com.redhat.pantheon.model.module.ModuleVersion; +import com.redhat.pantheon.servlet.util.ServletHelper; +import org.apache.sling.api.SlingHttpServletRequest; + +import javax.jcr.RepositoryException; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static com.google.common.collect.Maps.newHashMap; +import static com.redhat.pantheon.conf.GlobalConfig.CONTENT_TYPE; +import static javax.servlet.http.HttpServletResponse.SC_OK; + +/** + * A series of converters to map form for different business purposes. + */ +public class MapConverters { + + private MapConverters() { + } + + /** + * Converts a {@link ModuleVariant} object to a map for returning in API calls. + * @param request The web request being processed. + * @param mv The module variant domain object to transform to a map. + * @return A map for Json conversion in API calls with the module variant's information. + * @throws RepositoryException IF there is a problem fetching related data when building the map. + */ + public static final Map moduleVariantToMap(final SlingHttpServletRequest request, + final ModuleVariant mv) + throws RepositoryException { + Optional releasedMetadata = mv.released() + .toChild(ModuleVersion::metadata) + .asOptional(); + Optional releasedContent = mv.released() + .toChild(ModuleVersion::cachedHtml) + .asOptional(); + Optional releasedRevision = mv.released() + .asOptional(); + + Map variantMap = newHashMap(mv.getValueMap()); + Map variantDetails = new HashMap<>(); + + variantDetails.put("status", SC_OK); + variantDetails.put("message", "Module Found"); + + String resourcePath = mv.getPath(); + variantMap.put("locale", ServletUtils.toLanguageTag(mv.getParentLocale().getName())); + variantMap.put("revision_id", releasedRevision.get().getName()); + variantMap.put("title", releasedMetadata.get().title().get()); + variantMap.put("headline", releasedMetadata.get().getValueMap().containsKey("pant:headline") ? releasedMetadata.get().headline().get() : ""); + variantMap.put("description", releasedMetadata.get().getValueMap().containsKey("jcr:description") ? releasedMetadata.get().description().get() : releasedMetadata.get().mAbstract().get()); + variantMap.put("content_type", CONTENT_TYPE); + variantMap.put("date_published", releasedMetadata.get().getValueMap().containsKey("pant:datePublished") ? releasedMetadata.get().datePublished().get().toInstant().toString() : ""); + variantMap.put("date_first_published", releasedMetadata.get().getValueMap().containsKey("pant:dateFirstPublished") ? releasedMetadata.get().dateFirstPublished().get().toInstant().toString() : ""); + variantMap.put("status", "published"); + + // Assume the path is something like: /content//my/resource/path + variantMap.put("module_url_fragment", resourcePath.substring("/content/repositories/".length())); + + // Striping out the jcr: from key name + String variant_uuid = (String) variantMap.remove("jcr:uuid"); + // TODO: remove uuid when there are no more consumers for it (Solr, Hydra, Customer Portal) + variantMap.put("uuid", variant_uuid); + variantMap.put("variant_uuid", variant_uuid); + variantMap.put("document_uuid", mv.getParentLocale().getParent().uuid().get()); + // Convert date string to UTC + Date dateModified = new Date(mv.getResourceMetadata().getModificationTime()); + variantMap.put("date_modified", dateModified.toInstant().toString()); + // Return the body content of the module ONLY + variantMap.put("body", + Html.parse(Charsets.UTF_8.name()) + .andThen(Html.rewriteUuidUrls(request.getResourceResolver(), new CustomerPortalUrlUuidProvider())) + .andThen(Html.getBody()) + .apply(releasedContent.get().jcrContent().get().jcrData().get())); + + // Fields that are part of the spec and yet to be implemented + // TODO Should either of these be the variant name? + variantMap.put("context_url_fragment", ""); + variantMap.put("context_id", ""); + + // Process productVersion from metadata + // Making these arrays - in the future, we will have multi-product, so get the API right the first time + List productList = new ArrayList<>(); + variantMap.put("products", productList); + ProductVersion pv = releasedMetadata.get().productVersion().getReference(); + String versionUrlFragment = ""; + String productUrlFragment = ""; + if (pv != null) { + Map productMap = new HashMap<>(); + productList.add(productMap); + productMap.put("product_version", pv.name().get()); + versionUrlFragment = pv.getValueMap().containsKey("urlFragment") ? pv.urlFragment().get() : ""; + productMap.put("version_url_fragment", versionUrlFragment); + productUrlFragment = pv.getProduct().getValueMap().containsKey("urlFragment") ? pv.getProduct().urlFragment().get() : ""; + productMap.put("product_name", pv.getProduct().name().get()); + productMap.put("product_url_fragment", productUrlFragment); + } + + // Process url_fragment from metadata + String urlFragment = releasedMetadata.get().urlFragment().get() != null ? releasedMetadata.get().urlFragment().get() : ""; + if (!urlFragment.isEmpty()) { + variantMap.put("vanity_url_fragment", urlFragment); + } else { + variantMap.put("vanity_url_fragment", ""); + } + + String searchKeywords = releasedMetadata.get().searchKeywords().get(); + if (searchKeywords != null && !searchKeywords.isEmpty()) { + variantMap.put("search_keywords", searchKeywords.split(", *")); + } else { + variantMap.put("search_keywords", new String[]{}); + } + + // Process view_uri + if (System.getenv("portal_url") != null) { + String view_uri = new CustomerPortalUrlUuidProvider().generateUrlString(mv); + variantMap.put("view_uri", view_uri); + } else { + variantMap.put("view_uri", ""); + } + List> includeAssemblies = new ArrayList<>(); + + //get the assemblies and iterate over them + + ServletHelper.addAssemblyDetails(ServletHelper.getModuleUuidFromVariant(mv), includeAssemblies, request, false, false); + variantMap.put("included_in_guides", includeAssemblies); + variantMap.put("isPartOf", includeAssemblies); + // remove unnecessary fields from the map + variantMap.remove("jcr:lastModified"); + variantMap.remove("jcr:lastModifiedBy"); + variantMap.remove("jcr:createdBy"); + variantMap.remove("jcr:created"); + variantMap.remove("sling:resourceType"); + variantMap.remove("jcr:primaryType"); + + // Adding variantMap to a parent variantDetails map + variantDetails.put("module", variantMap); + + return variantDetails; + } +} diff --git a/pantheon-bundle/src/main/java/com/redhat/pantheon/servlet/module/VanityVariantJsonServlet.java b/pantheon-bundle/src/main/java/com/redhat/pantheon/servlet/module/VanityVariantJsonServlet.java new file mode 100644 index 000000000..747a8ec2e --- /dev/null +++ b/pantheon-bundle/src/main/java/com/redhat/pantheon/servlet/module/VanityVariantJsonServlet.java @@ -0,0 +1,143 @@ +package com.redhat.pantheon.servlet.module; + +import com.ibm.icu.util.ULocale; +import com.redhat.pantheon.jcr.JcrQueryHelper; +import com.redhat.pantheon.model.api.SlingModels; +import com.redhat.pantheon.model.module.ModuleVariant; +import com.redhat.pantheon.model.module.ModuleVersion; +import com.redhat.pantheon.servlet.MapConverters; +import com.redhat.pantheon.servlet.util.SlingPathSuffix; +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.SlingHttpServletResponse; +import org.apache.sling.api.servlets.SlingSafeMethodsServlet; +import org.apache.sling.servlets.annotations.SlingServletPaths; +import org.jetbrains.annotations.NotNull; +import org.osgi.framework.Constants; +import org.osgi.service.component.annotations.Component; + +import javax.jcr.RepositoryException; +import javax.servlet.Servlet; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Map; +import java.util.Optional; + +import static com.redhat.pantheon.servlet.ServletUtils.writeAsJson; + +/** + * An API endpoint for finding module information using a vanity url format which takes into account the content's + * product metadata, and locale. The format is as follows:

+ * + * '/api/module/vanity.json/{locale}/{product}/{version}/{vanityUrlFragment}'

+ * + * It will also resolve content with the following format:

+ * + * '/api/module/vanity.json/{locale}/{product}/{version}/{moduleUUID}' + * + *
+ * @see VariantJsonServlet for a servlet returning the same information + * @author Carlos Munoz + */ +@Component( + service = Servlet.class, + property = { + Constants.SERVICE_DESCRIPTION + "=Servlet to facilitate GET operation which accepts several path parameters to fetch module variant data", + Constants.SERVICE_VENDOR + "=Red Hat Content Tooling team" + }) +@SlingServletPaths(value = "/api/module/vanity") +// /api/module/vanity.json/{locale}/{productLabel}/{versionLabel}/{vanityUrl} +public class VanityVariantJsonServlet extends SlingSafeMethodsServlet { + + private final SlingPathSuffix suffix = new SlingPathSuffix("/{locale}/{productLabel}/{versionLabel}/{vanityUrl}"); + + @Override + protected void doGet(@NotNull SlingHttpServletRequest request, @NotNull SlingHttpServletResponse response) throws ServletException, IOException { + Map params = suffix.getParameters(request); + String locale = params.get("locale"); + String productLabel = params.get("productLabel"); + String versionLabel = params.get("versionLabel"); + String vanityUrl = params.get("vanityUrl"); + + JcrQueryHelper queryHelper = new JcrQueryHelper(request.getResourceResolver()); + try { + + // Find the metadata with the right vanity url, + Optional moduleVersion = queryHelper.query( + "select * from [pant:moduleVersion] as v where v.[metadata/urlFragment] = '/" + vanityUrl + "'") + .map(resource -> SlingModels.getModel(resource, ModuleVersion.class)) + // the right locale, + .filter(modVer -> { + String normalizedLocaleCode = ULocale.canonicalize(locale); + String normalizedModuleLocaleCode = ULocale.canonicalize(modVer.getParent().getParentLocale().getName()); + return normalizedLocaleCode.equals(normalizedModuleLocaleCode); + }) + // the right version, + .filter(modVer -> { + try { + return modVer.metadata().get().productVersion().getReference().name().get().equals(versionLabel); + } catch (RepositoryException e) { + throw new RuntimeException(e); + } + }) + // and the right product + .filter(modVer -> { + try { + return modVer.metadata().get().productVersion().getReference().getProduct().urlFragment().get().equals(productLabel); + } catch (RepositoryException e) { + throw new RuntimeException(e); + } + }) + // There should be 1 at most, but get the first if there are more + .findFirst(); + + if(!moduleVersion.isPresent()) { + // try to find by variant uuid, instead of vanity url + moduleVersion = findModuleByUuid(queryHelper, productLabel, versionLabel, locale, vanityUrl); + } + + if(!moduleVersion.isPresent()) { + response.sendError(HttpServletResponse.SC_NOT_FOUND, "Module version with vanity url '" + vanityUrl + "' not found"); + return; + } + else { + // TODO This is traversing up to the variant to keep the compatibility with VariantJsonServlet + // it should be revisited if/after this api is deprecated + writeAsJson(response, MapConverters.moduleVariantToMap(request, moduleVersion.get().getParent())); + } + + } catch (RepositoryException e) { + throw new RuntimeException(e); + } + } + + // TODO This could just piggy back on the other variant json servlet (but then there is no validation) + private Optional findModuleByUuid(final JcrQueryHelper queryHelper, + final String productUrlFragment, + final String productVersionUrlFragment, + final String locale, + final String moduleVariantUuid) { + try { + return queryHelper.query("select * from [pant:moduleVariant] as moduleVariant WHERE moduleVariant.[jcr:uuid] = '" + + moduleVariantUuid + "'") + .map(resource -> SlingModels.getModel(resource, ModuleVariant.class)) + // check the other parameters match + .filter(moduleVariant -> { + try { + return ULocale.canonicalize(moduleVariant.getParentLocale().getName()).equals(ULocale.canonicalize(locale)) + && moduleVariant.released().get().metadata().get().productVersion().getReference().urlFragment().get() + .equals(productVersionUrlFragment) + && moduleVariant.released().get().metadata().get().productVersion().getReference().getProduct() + .urlFragment().get().equals(productUrlFragment); + } catch (RepositoryException e) { + throw new RuntimeException(e); + } + }) + // use the released version to keep compatibility with VariantJsonServlet + .map(moduleVariant -> moduleVariant.released().get()) + .findFirst(); + } catch (RepositoryException e) { + throw new RuntimeException(e); + } + } +}