View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.doxia.site.inheritance;
20  
21  import javax.inject.Named;
22  import javax.inject.Singleton;
23  
24  import java.util.ArrayList;
25  import java.util.List;
26  
27  import org.apache.maven.doxia.site.Body;
28  import org.apache.maven.doxia.site.LinkItem;
29  import org.apache.maven.doxia.site.Logo;
30  import org.apache.maven.doxia.site.Menu;
31  import org.apache.maven.doxia.site.MenuItem;
32  import org.apache.maven.doxia.site.SiteModel;
33  import org.codehaus.plexus.util.xml.Xpp3Dom;
34  
35  /**
36   * Manage inheritance of the site model.
37   *
38   * @author <a href="mailto:brett@apache.org">Brett Porter</a>
39   * @author <a href="mailto:henning@apache.org">Henning P. Schmiedehausen</a>
40   */
41  @Singleton
42  @Named
43  public class DefaultSiteModelInheritanceAssembler implements SiteModelInheritanceAssembler {
44      /** {@inheritDoc} */
45      public void assembleModelInheritance(
46              String name, SiteModel child, SiteModel parent, String childBaseUrl, String parentBaseUrl) {
47          if (parent == null || !child.isMergeParent()) {
48              return;
49          }
50  
51          child.setCombineSelf(parent.getCombineSelf());
52  
53          URLRebaser urlContainer = new URLRebaser(parentBaseUrl, childBaseUrl);
54  
55          if (child.getBannerLeft() == null && parent.getBannerLeft() != null) {
56              child.setBannerLeft(parent.getBannerLeft().clone());
57              rebaseLinkItemPaths(child.getBannerLeft(), urlContainer);
58          }
59  
60          if (child.getBannerRight() == null && parent.getBannerRight() != null) {
61              child.setBannerRight(parent.getBannerRight().clone());
62              rebaseLinkItemPaths(child.getBannerRight(), urlContainer);
63          }
64  
65          if (child.isDefaultPublishDate() && parent.getPublishDate() != null) {
66              child.setPublishDate(parent.getPublishDate().clone());
67          }
68  
69          if (child.isDefaultVersion() && parent.getVersion() != null) {
70              child.setVersion(parent.getVersion().clone());
71          }
72  
73          if (child.getEdit() == null && parent.getEdit() != null) {
74              child.setEdit(parent.getEdit());
75          }
76  
77          if (child.getSkin() == null && parent.getSkin() != null) {
78              child.setSkin(parent.getSkin().clone());
79          }
80  
81          child.setPoweredBy(mergePoweredByLists(child.getPoweredBy(), parent.getPoweredBy(), urlContainer));
82  
83          if (parent.getLastModified() > child.getLastModified()) {
84              child.setLastModified(parent.getLastModified());
85          }
86  
87          assembleBodyInheritance(name, child, parent, urlContainer);
88  
89          assembleCustomInheritance(child, parent);
90      }
91  
92      /** {@inheritDoc} */
93      public void resolvePaths(final SiteModel siteModel, final String baseUrl) {
94          if (baseUrl == null) {
95              return;
96          }
97  
98          if (siteModel.getBannerLeft() != null) {
99              relativizeLinkItemPaths(siteModel.getBannerLeft(), baseUrl);
100         }
101 
102         if (siteModel.getBannerRight() != null) {
103             relativizeLinkItemPaths(siteModel.getBannerRight(), baseUrl);
104         }
105 
106         for (Logo logo : siteModel.getPoweredBy()) {
107             relativizeLinkItemPaths(logo, baseUrl);
108         }
109 
110         if (siteModel.getBody() != null) {
111             for (LinkItem linkItem : siteModel.getBody().getLinks()) {
112                 relativizeLinkItemPaths(linkItem, baseUrl);
113             }
114 
115             for (LinkItem linkItem : siteModel.getBody().getBreadcrumbs()) {
116                 relativizeLinkItemPaths(linkItem, baseUrl);
117             }
118 
119             for (Menu menu : siteModel.getBody().getMenus()) {
120                 relativizeMenuPaths(menu.getItems(), baseUrl);
121                 if (menu.getImage() != null) {
122                     menu.getImage().setSrc(relativizeLink(menu.getImage().getSrc(), baseUrl));
123                 }
124             }
125         }
126     }
127 
128     private void assembleCustomInheritance(final SiteModel child, final SiteModel parent) {
129         if (child.getCustom() == null) {
130             child.setCustom(parent.getCustom());
131         } else {
132             child.setCustom(Xpp3Dom.mergeXpp3Dom((Xpp3Dom) child.getCustom(), (Xpp3Dom) parent.getCustom()));
133         }
134     }
135 
136     private void assembleBodyInheritance(
137             final String name, final SiteModel child, final SiteModel parent, final URLRebaser urlContainer) {
138         Body cBody = child.getBody();
139         Body pBody = parent.getBody();
140 
141         if (cBody != null || pBody != null) {
142             if (cBody == null) {
143                 cBody = new Body();
144                 child.setBody(cBody);
145             }
146 
147             if (pBody == null) {
148                 pBody = new Body();
149             }
150 
151             if (cBody.getHead() == null && pBody.getHead() != null) {
152                 cBody.setHead(pBody.getHead());
153             }
154 
155             cBody.setLinks(mergeLinkItemLists(cBody.getLinks(), pBody.getLinks(), urlContainer, false));
156 
157             if (cBody.getBreadcrumbs().isEmpty() && !pBody.getBreadcrumbs().isEmpty()) {
158                 LinkItem breadcrumb = new LinkItem();
159                 breadcrumb.setName(name);
160                 breadcrumb.setHref("index.html");
161                 cBody.getBreadcrumbs().add(breadcrumb);
162             }
163             cBody.setBreadcrumbs(
164                     mergeLinkItemLists(cBody.getBreadcrumbs(), pBody.getBreadcrumbs(), urlContainer, true));
165 
166             cBody.setMenus(mergeMenus(cBody.getMenus(), pBody.getMenus(), urlContainer));
167 
168             if (cBody.getFooter() == null && pBody.getFooter() != null) {
169                 cBody.setFooter(pBody.getFooter());
170             }
171         }
172     }
173 
174     private List<Menu> mergeMenus(
175             final List<Menu> childMenus, final List<Menu> parentMenus, final URLRebaser urlContainer) {
176         List<Menu> menus = new ArrayList<>(childMenus.size() + parentMenus.size());
177 
178         for (Menu menu : childMenus) {
179             menus.add(menu);
180         }
181 
182         int topCounter = 0;
183         for (Menu menu : parentMenus) {
184             if ("top".equals(menu.getInherit())) {
185                 final Menu clone = menu.clone();
186 
187                 rebaseMenuPaths(clone.getItems(), urlContainer);
188                 if (clone.getImage() != null) {
189                     clone.getImage()
190                             .setSrc(urlContainer.rebaseLink(clone.getImage().getSrc()));
191                 }
192 
193                 menus.add(topCounter, clone);
194                 topCounter++;
195             } else if ("bottom".equals(menu.getInherit())) {
196                 final Menu clone = menu.clone();
197 
198                 rebaseMenuPaths(clone.getItems(), urlContainer);
199                 if (clone.getImage() != null) {
200                     clone.getImage()
201                             .setSrc(urlContainer.rebaseLink(clone.getImage().getSrc()));
202                 }
203 
204                 menus.add(clone);
205             }
206         }
207 
208         return menus;
209     }
210 
211     private void relativizeMenuPaths(final List<MenuItem> items, final String baseUrl) {
212         for (MenuItem item : items) {
213             relativizeLinkItemPaths(item, baseUrl);
214             relativizeMenuPaths(item.getItems(), baseUrl);
215         }
216     }
217 
218     private void rebaseMenuPaths(final List<MenuItem> items, final URLRebaser urlContainer) {
219         for (MenuItem item : items) {
220             rebaseLinkItemPaths(item, urlContainer);
221             rebaseMenuPaths(item.getItems(), urlContainer);
222         }
223     }
224 
225     private void relativizeLinkItemPaths(final LinkItem item, final String baseUrl) {
226         item.setHref(relativizeLink(item.getHref(), baseUrl));
227         if (item.getImage() != null) {
228             item.getImage().setSrc(relativizeLink(item.getImage().getSrc(), baseUrl));
229         }
230     }
231 
232     private void rebaseLinkItemPaths(final LinkItem item, final URLRebaser urlContainer) {
233         item.setHref(urlContainer.rebaseLink(item.getHref()));
234         if (item.getImage() != null) {
235             item.getImage().setSrc(urlContainer.rebaseLink(item.getImage().getSrc()));
236         }
237     }
238 
239     private List<LinkItem> mergeLinkItemLists(
240             final List<LinkItem> childList,
241             final List<LinkItem> parentList,
242             final URLRebaser urlContainer,
243             boolean cutParentAfterDuplicate) {
244         List<LinkItem> items = new ArrayList<>(childList.size() + parentList.size());
245 
246         for (LinkItem item : parentList) {
247             if (!items.contains(item) && !childList.contains(item)) {
248                 final LinkItem clone = item.clone();
249 
250                 rebaseLinkItemPaths(clone, urlContainer);
251 
252                 items.add(clone);
253             } else if (cutParentAfterDuplicate) {
254                 // if a parent item is found in child, ignore next items (case for breadcrumbs)
255                 // merge ( "B > E", "A > B > C > D" ) -> "A > B > E" (notice missing "C > D")
256                 // see https://issues.apache.org/jira/browse/DOXIASITETOOLS-62
257                 break;
258             }
259         }
260 
261         for (LinkItem item : childList) {
262             if (!items.contains(item)) {
263                 items.add(item);
264             }
265         }
266 
267         return items;
268     }
269 
270     private List<Logo> mergePoweredByLists(
271             final List<Logo> childList, final List<Logo> parentList, final URLRebaser urlContainer) {
272         List<Logo> logos = new ArrayList<>(childList.size() + parentList.size());
273 
274         for (Logo logo : parentList) {
275             if (!logos.contains(logo)) {
276                 final Logo clone = logo.clone();
277 
278                 rebaseLinkItemPaths(clone, urlContainer);
279 
280                 logos.add(clone);
281             }
282         }
283 
284         for (Logo logo : childList) {
285             if (!logos.contains(logo)) {
286                 logos.add(logo);
287             }
288         }
289 
290         return logos;
291     }
292 
293     // relativize only affects absolute links, if the link has the same scheme, host and port
294     // as the base, it is made into a relative link as viewed from the base
295     private String relativizeLink(final String link, final String baseUri) {
296         if (link == null || baseUri == null) {
297             return link;
298         }
299 
300         // this shouldn't be necessary, just to swallow mal-formed hrefs
301         try {
302             final URIPathDescriptor path = new URIPathDescriptor(baseUri, link);
303 
304             return path.relativizeLink().toString();
305         } catch (IllegalArgumentException e) {
306             return link;
307         }
308     }
309 
310     /**
311      * URL rebaser: based on an old and a new path, can rebase a link based on old path to a value based on the new
312      * path.
313      */
314     private static class URLRebaser {
315 
316         private final String oldPath;
317 
318         private final String newPath;
319 
320         /**
321          * Construct a URL rebaser.
322          *
323          * @param oldPath the old path.
324          * @param newPath the new path.
325          */
326         URLRebaser(final String oldPath, final String newPath) {
327             this.oldPath = oldPath;
328             this.newPath = newPath;
329         }
330 
331         /**
332          * Get the new path.
333          *
334          * @return the new path.
335          */
336         public String getNewPath() {
337             return this.newPath;
338         }
339 
340         /**
341          * Get the old path.
342          *
343          * @return the old path.
344          */
345         public String getOldPath() {
346             return this.oldPath;
347         }
348 
349         /**
350          * Rebase only affects relative links, a relative link wrt an old base gets translated,
351          * so it points to the same location as viewed from a new base
352          */
353         public String rebaseLink(final String link) {
354             if (link == null || getOldPath() == null) {
355                 return link;
356             }
357 
358             if (link.contains("${project.")) {
359                 throw new IllegalArgumentException("site.xml late interpolation ${project.*} expression found"
360                         + " in link: '" + link + "'. Use early interpolation ${this.*}");
361             }
362 
363             final URIPathDescriptor oldPath = new URIPathDescriptor(getOldPath(), link);
364 
365             return oldPath.rebaseLink(getNewPath()).toString();
366         }
367     }
368 }