001/* 002 * SPDX-License-Identifier: Apache-2.0 003 * 004 * Copyright 2023-2026 The Enola <https://enola.dev> Authors 005 * 006 * Licensed under the Apache License, Version 2.0 (the "License"); 007 * you may not use this file except in compliance with the License. 008 * You may obtain a copy of the License at 009 * 010 * https://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package dev.enola.web; 019 020import com.google.common.escape.Escaper; 021import com.google.common.html.HtmlEscapers; 022 023import dev.enola.thing.gen.LinkTransformer; 024import dev.enola.thing.message.ProtoThingMetadataProvider; 025import dev.enola.thing.proto.ThingOrBuilder; 026import dev.enola.thing.proto.Value; 027import dev.enola.thing.proto.Value.List; 028import dev.enola.thing.proto.Value.Literal; 029 030import java.util.Map; 031 032public class ThingUI { 033 034 // See https://github.com/google/google-java-format/issues/1033 re. using moar STR formatting ;( 035 036 // We intentionally don't use any (other) template engine here; see e.g. 037 // https://blog.machinezoo.com/template-engines-broken for why. 038 // TODO Or rewrite using e.g. Mustache? Client Side? ;) 039 040 // TODO Use Appendable-based approach, for better memory efficiency, and less String "trashing" 041 042 private final ProtoThingMetadataProvider metadataProvider; 043 private final LinkTransformer linkTransformer; 044 045 public ThingUI(ProtoThingMetadataProvider metadataProvider, LinkTransformer linkTransformer) { 046 this.metadataProvider = metadataProvider; 047 this.linkTransformer = linkTransformer; 048 } 049 050 public CharSequence html(ThingOrBuilder thing) { 051 // TODO Print/include thing.getIri() on of HTML, but with initial / link 052 return table(thing.getPropertiesMap(), "thing"); 053 } 054 055 private CharSequence value(Value value, String tableCssClass) { 056 return switch (value.getKindCase()) { 057 case STRING -> s(value.getString()); 058 case LANG_STRING -> 059 s(value.getLangString().getText() + "@" + value.getLangString().getLang()); 060 case LINK -> link(value.getLink()); 061 case LITERAL -> literal(value.getLiteral()); 062 case STRUCT -> table(value.getStruct().getPropertiesMap(), tableCssClass); 063 case LIST -> list(value.getList()); 064 case KIND_NOT_SET -> ""; 065 default -> 066 throw new IllegalArgumentException("Unexpected value: " + value.getKindCase()); 067 }; 068 } 069 070 private CharSequence literal(Literal literal) { 071 // TODO Use a dev.enola.common.convert.Converter based on the literal.getDatatype() 072 return "<span title=" + literal.getDatatype() + ">" + s(literal.getValue()) + "</span>"; 073 } 074 075 private CharSequence table(Map<String, Value> fieldsMap, String cssClass) { 076 var sb = new StringBuilder("<table"); 077 if (!cssClass.isEmpty()) { 078 sb.append(" class=\"").append(s(cssClass)).append("\""); 079 } 080 sb.append("><tbody>\n"); 081 for (var nameValue : fieldsMap.entrySet()) { 082 sb.append("<tr>\n"); 083 sb.append("<td class=\"label\">").append(link(nameValue.getKey())).append("</td>"); 084 sb.append("<td>").append(value(nameValue.getValue(), "")).append("</td>"); 085 sb.append("</tr>\n"); 086 } 087 sb.append("</tbody></table>\n"); 088 return sb; 089 } 090 091 private CharSequence list(List list) { 092 var sb = new StringBuilder(); 093 sb.append("<ol>\n"); 094 for (var value : list.getValuesList()) { 095 sb.append("<li>"); 096 sb.append(value(value, "")); 097 sb.append("</li>\n"); 098 } 099 sb.append("</ol>\n"); 100 return sb; 101 } 102 103 private CharSequence link(String iri) { 104 var meta = metadataProvider.get(iri); 105 var sb = new StringBuilder(); 106 sb.append(meta.imageHTML()); 107 sb.append(' '); 108 // TODO s(uri) or not - or another escaping? 109 sb.append("<a href=" + s(linkTransformer.get(iri))); 110 var description = meta.descriptionHTML(); 111 if (!description.isEmpty()) { 112 sb.append(" title=\""); 113 sb.append(s(description)); 114 sb.append('"'); 115 } 116 sb.append('>'); 117 sb.append(s(meta.label())); 118 sb.append("</a>"); 119 return sb; 120 } 121 122 private static final Escaper htmlEscaper = HtmlEscapers.htmlEscaper(); 123 124 /** Santize raw text to be safe HTML. */ 125 // TODO Implement Escaper as StringTemplate.Processor? 126 // (See e.g. https://javaalmanac.io/features/stringtemplates/) 127 private static String s(String raw) { 128 // Replacement of "#" by "%23" is required so that links such as e.g 129 // http://[::]:8080/ui/http://www.w3.org/1999/02/22-rdf-syntax-ns#subject 130 // et al. work as needed! 131 return htmlEscaper.escape(raw).replace("#", "%23"); 132 } 133}