/** * Copyright 2013 the original author or authors. * * Licensed under the Apache License, Version 2.0 the "License"; * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. **/ package io.neba.core.resourcemodels.registration; import io.neba.core.util.OsgiBeanSource; import org.apache.felix.webconsole.AbstractWebConsolePlugin; import org.apache.sling.api.resource.LoginException; import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.api.resource.ResourceResolverFactory; import org.apache.sling.commons.json.JSONArray; import org.apache.sling.commons.json.JSONException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.net.URL; import java.util.*; import java.util.Map.Entry; import java.util.stream.Collectors; import static io.neba.core.util.BundleUtil.displayNameOf; import static io.neba.core.util.ClassHierarchyIterator.hierarchyOf; import static java.lang.Character.isUpperCase; import static org.apache.commons.io.IOUtils.closeQuietly; import static org.apache.commons.io.IOUtils.copy; import static org.apache.commons.lang.StringUtils.*; import static org.apache.sling.api.resource.ResourceUtil.*; /** * Shows a table with all detected type -> model mappings in the felix console. * * @author Olaf Otto */ @Service public class ModelRegistryConsolePlugin extends AbstractWebConsolePlugin { public static final String LABEL = "modelregistry"; public static final String PREFIX_STATIC = "/static"; private static final long serialVersionUID = -8676958166611686979L; private static final String API_PATH = "/api"; private static final String API_FILTER = "/filter"; private static final String API_RESOURCES = "/resources"; private static final String API_COMPONENTICON = "/componenticon"; private static final String API_MODELTYPES = "/modeltypes"; private static final String PARAM_TYPENAME = "modelTypeName"; private static final String PARAM_PATH = "path"; @Autowired private ResourceResolverFactory resourceResolverFactory; @Autowired private ModelRegistry registry; @SuppressWarnings("unused") public String getCategory() { return "NEBA"; } @Override public String getLabel() { return LABEL; } @Override public String getTitle() { return "Model registry"; } @Override protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { String suffix = substringAfter(req.getRequestURI(), req.getServletPath() + "/" + getLabel()); if (!isBlank(suffix) && suffix.startsWith(API_PATH)) { handleApiCall(suffix.substring(API_PATH.length()), req, res); return; } super.doGet(req, res); } private void handleApiCall(String apiIdentifier, HttpServletRequest req, HttpServletResponse res) throws IOException { try { if (apiIdentifier.startsWith(API_COMPONENTICON)) { spoolComponentIcon(res, apiIdentifier); return; } res.setContentType("application/json;charset=UTF-8"); if (apiIdentifier.startsWith(API_FILTER)) { provideFilteredModelRegistryView(req, res); return; } if (apiIdentifier.startsWith(API_RESOURCES)) { provideMatchingResourcePaths(req, res); return; } if (apiIdentifier.startsWith(API_MODELTYPES)) { provideAllModelTypes(res); } } catch (JSONException e) { throw new IllegalStateException("Unable to render JSON response.", e); } } @Override protected void renderContent(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { writeHeadnavigation(res); PrintWriter writer = res.getWriter(); writeScriptIncludes(res); writer.write("<table id=\"plugin_table\" class=\"nicetable tablesorter noauto\">"); writer.write("<thead><tr><th>Type</th><th>Model type</th><th>Bean name</th><th>Source bundle</th></tr></thead>"); writer.write("<tbody>"); for (Entry<String, Collection<OsgiBeanSource<?>>> entry : this.registry.getTypeMappings().entrySet()) { for (OsgiBeanSource<?> source : entry.getValue()) { String sourceBundleName = displayNameOf(source.getBundle()); writer.write("<tr data-modeltype=\"" + source.getBeanType().getName() + "\">"); String resourceType = buildCrxDeLinkToResourceType(req, entry.getKey()); writer.write("<td>" + resourceType + "</td>"); writer.write("<td>" + source.getBeanType().getName() + "</td>"); writer.write("<td>" + source.getBeanName() + "</td>"); writer.write("<td><a href=\"bundles/" + source.getBundleId() + "\" " + "title=\"" + sourceBundleName + "\">" + source.getBundleId() + "</a></td>"); writer.write("</tr>"); } } writer.write("</tbody>"); writer.write("</table>"); } private String buildCrxDeLinkToResourceType(HttpServletRequest request, String type) { ResourceResolver resolver = getResourceResolver(); try { String path = resourceTypeToPath(type); Resource resource = null; Resource iconResource = null; for (String searchPath : resolver.getSearchPath()) { resource = resolver.getResource(searchPath + path); if (resource != null && !isNonExistingResource(resource) && !isSyntheticResource(resource)) { iconResource = resource.getChild("icon.png"); break; } } return resource != null ? "<a href=\"" + request.getContextPath() + "/crx/de/#" + resource.getPath() + "\" " + "class=\"crxdelink\">" + "<img class=\"componentIcon\" src=\"" + getLabel() + API_PATH + API_COMPONENTICON + (iconResource == null ? "" : resource.getPath()) + "\"/>" + type + "</a>" : "<span class=\"unresolved\">" + type + "</span>"; } finally { resolver.close(); } } private void provideAllModelTypes(HttpServletResponse res) throws IOException, JSONException { Set<String> typeNames = new HashSet<>(); for (OsgiBeanSource<?> source: this.registry.getBeanSources()) { for (Class<?> type : hierarchyOf(source.getBeanType())) { if (type == Object.class) { continue; } typeNames.add(type.getName()); } } new JSONArray(typeNames).write(res.getWriter()); } private void provideMatchingResourcePaths(HttpServletRequest req, HttpServletResponse res) throws IOException, JSONException { String path = req.getParameter(PARAM_PATH); if (isEmpty(path) || path.charAt(0) != '/') { return; } ResourceResolver resolver = getResourceResolver(); try { int idx = path.lastIndexOf('/'); Resource parent; String prefix = ""; if (idx < 1) { parent = resolver.getResource("/"); prefix = path.substring(1); } else { parent = resolver.getResource(path.substring(0, idx)); if (idx < path.length() - 1) { prefix = path.substring(idx + 1); } } if (parent == null) { return; } JSONArray array = new JSONArray(); Iterator<Resource> children = parent.listChildren(); while (children.hasNext()) { Resource child = children.next(); if (prefix.isEmpty() || child.getName().startsWith(prefix)) { array.put(child.getPath()); } } array.write(res.getWriter()); } finally { resolver.close(); } } private ResourceResolver getResourceResolver() { try { return this.resourceResolverFactory.getAdministrativeResourceResolver(null); } catch (LoginException e) { throw new IllegalStateException(e); } } private void provideFilteredModelRegistryView(HttpServletRequest req, HttpServletResponse res) throws IOException, JSONException { String modelTypePrefix = req.getParameter(PARAM_TYPENAME); String resourcePath = req.getParameter(PARAM_PATH); Collection<OsgiBeanSource<?>> types; if (isEmpty(resourcePath)) { types = this.registry.getBeanSources(); } else { types = resolveModelTypesFor(resourcePath); } Set<String> matchingModelTypeNames = new HashSet<>(64); String typeNameCandidate = substringAfterLast(modelTypePrefix, "."); boolean exactMatch = !isEmpty(typeNameCandidate) && isUpperCase(typeNameCandidate.charAt(0)); for (OsgiBeanSource<?> source : types) { if (modelTypePrefix == null) { matchingModelTypeNames.add(source.getBeanType().getName()); } else { for (Class<?> type : hierarchyOf(source.getBeanType())) { String typeName = type.getName(); if ((exactMatch ? typeName.equals(modelTypePrefix) : typeName.startsWith(modelTypePrefix))) { matchingModelTypeNames.add(source.getBeanType().getName()); break; } } } } new JSONArray(matchingModelTypeNames).write(res.getWriter()); } private Collection<OsgiBeanSource<?>> resolveModelTypesFor(String resourcePath) { Collection<OsgiBeanSource<?>> types = new ArrayList<>(64); if (!isEmpty(resourcePath)) { ResourceResolver resolver = getResourceResolver(); try { Resource resource = resolver.getResource(resourcePath); if (resource == null) { return types; } Collection<LookupResult> lookupResults = this.registry.lookupAllModels(resource); if (lookupResults == null) { return types; } types.addAll(lookupResults.stream().map(LookupResult::getSource).collect(Collectors.toList())); } finally { resolver.close(); } } return types; } public URL getResource(String path) { String internalPath = substringAfter(path, "/" + getLabel()); if (startsWith(internalPath, PREFIX_STATIC)) { return getClass().getResource("/META-INF/consoleplugin/modelregistry" + internalPath); } return null; } private void writeScriptIncludes(HttpServletResponse response) throws IOException { response.getWriter().write("<script src=\"" + getLabel() + "/static/script.js\"></script>"); } private void writeHeadnavigation(HttpServletResponse response) throws IOException { String template = readTemplateFile("/META-INF/consoleplugin/modelregistry/templates/head.html"); response.getWriter().printf(template, getNumberOfModels()); } private Object getNumberOfModels() { return this.registry.getBeanSources().size(); } private void spoolComponentIcon(HttpServletResponse response, String suffix) throws IOException { response.setContentType("image/png"); String iconPath = suffix.substring(API_COMPONENTICON.length()); if (iconPath.isEmpty()) { InputStream in = getClass().getResourceAsStream("/META-INF/consoleplugin/modelregistry/static/noicon.png"); try { copy(in, response.getOutputStream()); } finally { closeQuietly(in); } return; } ResourceResolver resolver = getResourceResolver(); InputStream in = null; try { Resource componentIcon = resolver.getResource(iconPath + "/icon.png"); if (componentIcon != null) { in = componentIcon.adaptTo(InputStream.class); copy(in, response.getOutputStream()); } } finally { resolver.close(); closeQuietly(in); } } }