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}