View Javadoc
1   /*
2   * Copyright 2012-2025 Christophe Friederich
3   *
4   * Licensed under the Apache License, Version 2.0 (the "License");
5   * you may not use this file except in compliance with the License.
6   * You may obtain a copy of the License at
7   *
8   * http://www.apache.org/licenses/LICENSE-2.0
9   *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16  package org.devacfr.maven.skins.reflow.snippet;
17  
18  import static java.util.Objects.requireNonNull;
19  
20  import com.google.common.base.MoreObjects;
21  import com.google.common.base.Strings;
22  import com.google.common.collect.Maps;
23  import com.google.common.collect.Sets;
24  import java.util.Map;
25  import java.util.Set;
26  import java.util.stream.Collectors;
27  import javax.annotation.Nonnull;
28  import javax.annotation.Nullable;
29  import org.jsoup.nodes.Attribute;
30  import org.jsoup.nodes.Attributes;
31  import org.jsoup.nodes.Element;
32  import org.jsoup.nodes.Node;
33  import org.jsoup.nodes.TextNode;
34  import org.jsoup.parser.Tag;
35  
36  /**
37   * Base of Snippet component.
38   *
39   * @author Christophe Friederich
40   * @version 2.4
41   * @param <T>
42   *          type of component
43   */
44  public class Component<T extends Component<T>> {
45  
46    private final static Set<String> knownTags = Sets.newHashSet("svg");
47  
48    /** */
49    private final Map<String, String> attributes = Maps.newHashMap();
50  
51    /** */
52    private Component<?> parent;
53  
54    /** */
55    private final Components children = new Components();
56  
57    /** */
58    private final Map<String, Components> childrenMap = Maps.newHashMap();
59  
60    /** */
61    private final Node node;
62  
63    /**
64     * @param node
65     *          a node (can <b>not</b> be {@code null}).
66     * @param parent
67     *          the parent component
68     * @return the created component
69     */
70    public static Component<?> createComponent(@Nonnull final Node node, final Component<?> parent) {
71      return new Component<>(node).withParent(parent).addAttributes(node.attributes());
72    }
73  
74    /**
75     * @param node
76     *          a node (can <b>not</b> be {@code null}).
77     */
78    protected Component(@Nonnull final Node node) {
79      this.node = requireNonNull(node);
80    }
81  
82    /**
83     * @return the name
84     */
85    public String getName() {
86      return node.nodeName();
87    }
88  
89    /**
90     * @return
91     */
92    public boolean isHtmlTag() {
93      return node instanceof TextNode || Tag.isKnownTag(node.nodeName()) || knownTags.contains(node.nodeName());
94    }
95  
96    /**
97     * @return the html content
98     */
99    public String getHtml() {
100     if (!isHtmlTag()) {
101       if (children.isEmpty()) {
102         return "";
103       } else {
104         return children.html();
105       }
106     } else {
107       return this.node.outerHtml();
108     }
109   }
110 
111   /**
112    * @return
113    */
114   public String getOwnHtml() {
115     if (!isHtmlTag()) {
116       return null;
117     } else {
118       return this.node.outerHtml();
119     }
120   }
121 
122   /**
123    * @return the parent
124    */
125   public Component<?> getParent() {
126     return parent;
127   }
128 
129   @Nullable protected Element getElement() {
130     if (this.node instanceof Element) {
131       return (Element) this.node;
132     }
133     return null;
134   }
135 
136   /**
137    * @param name
138    *          the name
139    * @return the attribute or child component value
140    */
141   public Object get(@Nonnull final String name) {
142     requireNonNull(name);
143     String key = name.toLowerCase();
144     // if attribute
145     if (this.attributes.containsKey(key)) {
146       return this.attributes.get(key);
147     } else {
148       // is children component?
149       // check if 's' suffix allowing to retrieve children component as list
150       if (key.endsWith("s") && !this.childrenMap.containsKey(key)) {
151         key = key.substring(0, key.length() - 1);
152         if (this.childrenMap.containsKey(key)) {
153           return this.childrenMap.get(key);
154         }
155       } else if (this.childrenMap.containsKey(key)) {
156         final Components value = this.childrenMap.get(key);
157         // TODO i don't know if good idea, but it's works.
158         if (value.size() > 1) {
159           return value;
160         }
161         return value.first();
162       }
163     }
164     return null;
165   }
166 
167   public Map<String, String> getAttrs() {
168     return this.attributes;
169   }
170 
171   public String getAttribute(String name) {
172     if (!this.attributes.containsKey(name)) {
173       return null;
174     }
175     return this.attributes.get(name);
176   }
177 
178   public boolean hasAttribute(String name) {
179     return this.attributes.containsKey(name);
180   }
181 
182   /**
183    * @return the ARIA attributes
184    */
185   public Map<String, String> getAriaAttributes() {
186     return this.attributes.entrySet()
187         .stream()
188         .filter(e -> e.getKey().startsWith("aria-"))
189         .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
190   }
191 
192   /**
193    * @return the data attributes
194    */
195   public Map<String, String> getDataAttributes() {
196     return this.attributes.entrySet()
197         .stream()
198         .filter(e -> e.getKey().startsWith("data-"))
199         .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
200   }
201 
202   /**
203    * @return the children
204    */
205   public Components getChildren() {
206     return children;
207   }
208 
209   public Components getChildren(final String name) {
210     final String key = requireNonNull(name).toLowerCase();
211     if (this.childrenMap.containsKey(key)) {
212       return this.childrenMap.get(key);
213     }
214     return Components.empty();
215   }
216 
217   public T addChild(final Component<?> component) {
218     final String key = component.getName();
219     Components value = null;
220     if (!this.childrenMap.containsKey(key)) {
221       value = new Components();
222       this.childrenMap.put(key, value);
223     } else {
224       value = this.childrenMap.get(key);
225     }
226     value.add(component);
227     this.children.add(component);
228     return self();
229   }
230 
231   protected T withParent(final Component<?> parent) {
232     this.parent = parent;
233     return self();
234   }
235 
236   @Nonnull
237   protected SnippetComponent<?> getRootParent() {
238     Component<?> parent = this.parent;
239     while (!(parent instanceof SnippetComponent<?>)) {
240       parent = parent.parent;
241     }
242     return (SnippetComponent<?>) parent;
243   }
244 
245   /**
246    * @param attrs
247    *          list of attributes
248    * @return the fluent instance
249    */
250   protected T addAttributes(@Nonnull final Attributes attrs) {
251     attrs.asList().stream().forEach(this::addAttribute);
252     return self();
253   }
254 
255   /**
256    * @param attr
257    *          attribute
258    * @return the fluent instance
259    */
260   protected T addAttribute(@Nonnull final Attribute attr) {
261     this.attributes.put(requireNonNull(attr.getKey()).toLowerCase(),
262       Strings.isNullOrEmpty(attr.getValue()) ? "true" : attr.getValue());
263     return self();
264   }
265 
266   /**
267    * @return the fluent instance
268    */
269   @SuppressWarnings("unchecked")
270   protected T self() {
271     return (T) this;
272   }
273 
274   /**
275    * {@inheritDoc}
276    */
277   @Override
278   public String toString() {
279     return MoreObjects.toStringHelper(this)
280         .add("name", this.getName())
281         .add("isHtmlTag", this.isHtmlTag())
282         .add("attributes", this.attributes)
283         .add("children", this.children.stream().map((cpt) -> cpt.getName()).collect(Collectors.toList()))
284         .toString();
285   }
286 }