-
Notifications
You must be signed in to change notification settings - Fork 37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add an initial revision of a semantic vanity url servlet #529
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<String, Object> moduleVariantToMap(final SlingHttpServletRequest request, | ||
final ModuleVariant mv) | ||
throws RepositoryException { | ||
Optional<ModuleMetadata> releasedMetadata = mv.released() | ||
.toChild(ModuleVersion::metadata) | ||
.asOptional(); | ||
Optional<FileResource> releasedContent = mv.released() | ||
.toChild(ModuleVersion::cachedHtml) | ||
.asOptional(); | ||
Optional<ModuleVersion> releasedRevision = mv.released() | ||
.asOptional(); | ||
|
||
Map<String, Object> variantMap = newHashMap(mv.getValueMap()); | ||
Map<String, Object> 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/<something>/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<Map> productList = new ArrayList<>(); | ||
variantMap.put("products", productList); | ||
ProductVersion pv = releasedMetadata.get().productVersion().getReference(); | ||
String versionUrlFragment = ""; | ||
String productUrlFragment = ""; | ||
if (pv != null) { | ||
Map<String, String> 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<HashMap<String, String>> 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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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:<br><br> | ||
* | ||
* '/api/module/vanity.json/{locale}/{product}/{version}/{vanityUrlFragment}'<br><br> | ||
* | ||
* It will also resolve content with the following format: <br><br> | ||
* | ||
* '/api/module/vanity.json/{locale}/{product}/{version}/{moduleUUID}' | ||
* | ||
* <br> | ||
* @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}"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the customer portal view_uri is constructed with product urlFragment and version urlFragment. not labels. Should we follow the same convention and use productUrlFragment and versionUrlFragment? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed. If you look at the code it is in fact filtering by url fragment. The strings are just pattern placeholders to be able to extract the right values from the url. I will change them to reflect that they are url fragments and not label. |
||
|
||
@Override | ||
protected void doGet(@NotNull SlingHttpServletRequest request, @NotNull SlingHttpServletResponse response) throws ServletException, IOException { | ||
Map<String, String> 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> moduleVersion = queryHelper.query( | ||
"select * from [pant:moduleVersion] as v where v.[metadata/urlFragment] = '/" + vanityUrl + "'") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A couple thoughts...
|
||
.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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In today's world, there could definitely be more than one. There's nothing stopping me from adding a vanity URL of "/cats" to every document in the system. My idea above of managing redirects as separate entities neatly addresses this problem. |
||
|
||
if(!moduleVersion.isPresent()) { | ||
// try to find by variant uuid, instead of vanity url | ||
moduleVersion = findModuleByUuid(queryHelper, productLabel, versionLabel, locale, vanityUrl); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm wondering why this "vanity" servlet is also accepting the UUID - perhaps you envision this new servlet as "the" one-and-only servlet that Drupal would call, and it handles anything you throw at it? Can you clarify if that's what you intended? |
||
} | ||
|
||
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<ModuleVersion> 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); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting that you register this servlet by path. I'm wondering if that doesn't overburden Drupal... Drupal would have to be smart enough to know whether the URL it was handling was a vanity URL or not and then call the correct Pantheon endpoint accordingly. That might be tough. I wonder if it doesn't make more sense for us to try and implement something more like a DefaultGetServlet for Sling that could handle the whole spectrum of requests and internally forward them to servlets appropriately.