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.decoration.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.decoration.Banner;
28  import org.apache.maven.doxia.site.decoration.Body;
29  import org.apache.maven.doxia.site.decoration.DecorationModel;
30  import org.apache.maven.doxia.site.decoration.LinkItem;
31  import org.apache.maven.doxia.site.decoration.Logo;
32  import org.apache.maven.doxia.site.decoration.Menu;
33  import org.apache.maven.doxia.site.decoration.MenuItem;
34  import org.codehaus.plexus.util.xml.Xpp3Dom;
35  
36  /**
37   * Manage inheritance of the decoration model.
38   *
39   * @author <a href="mailto:brett@apache.org">Brett Porter</a>
40   * @author <a href="mailto:henning@apache.org">Henning P. Schmiedehausen</a>
41   */
42  @Singleton
43  @Named
44  public class DefaultDecorationModelInheritanceAssembler implements DecorationModelInheritanceAssembler {
45      /** {@inheritDoc} */
46      public void assembleModelInheritance(
47              String name, DecorationModel child, DecorationModel parent, String childBaseUrl, String parentBaseUrl) {
48          if (parent == null || !child.isMergeParent()) {
49              return;
50          }
51  
52          child.setCombineSelf(parent.getCombineSelf());
53  
54          URLRebaser urlContainer = new URLRebaser(parentBaseUrl, childBaseUrl);
55  
56          if (child.getBannerLeft() == null && parent.getBannerLeft() != null) {
57              child.setBannerLeft(parent.getBannerLeft().clone());
58              rebaseBannerPaths(child.getBannerLeft(), urlContainer);
59          }
60  
61          if (child.getBannerRight() == null && parent.getBannerRight() != null) {
62              child.setBannerRight(parent.getBannerRight().clone());
63              rebaseBannerPaths(child.getBannerRight(), urlContainer);
64          }
65  
66          if (child.isDefaultPublishDate() && parent.getPublishDate() != null) {
67              child.setPublishDate(parent.getPublishDate().clone());
68          }
69  
70          if (child.isDefaultVersion() && parent.getVersion() != null) {
71              child.setVersion(parent.getVersion().clone());
72          }
73  
74          if (child.getEdit() == null && parent.getEdit() != null) {
75              child.setEdit(parent.getEdit());
76          }
77  
78          if (child.getSkin() == null && parent.getSkin() != null) {
79              child.setSkin(parent.getSkin().clone());
80          }
81  
82          child.setPoweredBy(mergePoweredByLists(child.getPoweredBy(), parent.getPoweredBy(), urlContainer));
83  
84          if (parent.getLastModified() > child.getLastModified()) {
85              child.setLastModified(parent.getLastModified());
86          }
87  
88          assembleBodyInheritance(name, child, parent, urlContainer);
89  
90          assembleCustomInheritance(child, parent);
91      }
92  
93      /** {@inheritDoc} */
94      public void resolvePaths(final DecorationModel decoration, final String baseUrl) {
95          if (baseUrl == null) {
96              return;
97          }
98  
99          if (decoration.getBannerLeft() != null) {
100             relativizeBannerPaths(decoration.getBannerLeft(), baseUrl);
101         }
102 
103         if (decoration.getBannerRight() != null) {
104             relativizeBannerPaths(decoration.getBannerRight(), baseUrl);
105         }
106 
107         for (Logo logo : decoration.getPoweredBy()) {
108             relativizeLogoPaths(logo, baseUrl);
109         }
110 
111         if (decoration.getBody() != null) {
112             for (LinkItem linkItem : decoration.getBody().getLinks()) {
113                 relativizeLinkItemPaths(linkItem, baseUrl);
114             }
115 
116             for (LinkItem linkItem : decoration.getBody().getBreadcrumbs()) {
117                 relativizeLinkItemPaths(linkItem, baseUrl);
118             }
119 
120             for (Menu menu : decoration.getBody().getMenus()) {
121                 relativizeMenuPaths(menu.getItems(), baseUrl);
122             }
123         }
124     }
125 
126     /**
127      * Resolves all relative paths between the elements in a banner. The banner element might contain relative paths
128      * to the oldBaseUrl, these are changed to the newBannerUrl.
129      *
130      * @param banner
131      * @param baseUrl
132      */
133     private void relativizeBannerPaths(final Banner banner, final String baseUrl) {
134         // banner has been checked to be not null, both href and src may be empty or null
135         banner.setHref(relativizeLink(banner.getHref(), baseUrl));
136         banner.setSrc(relativizeLink(banner.getSrc(), baseUrl));
137     }
138 
139     private void rebaseBannerPaths(final Banner banner, final URLRebaser urlContainer) {
140         if (banner.getHref() != null) // it may be empty
141         {
142             banner.setHref(urlContainer.rebaseLink(banner.getHref()));
143         }
144 
145         if (banner.getSrc() != null) {
146             banner.setSrc(urlContainer.rebaseLink(banner.getSrc()));
147         }
148     }
149 
150     private void assembleCustomInheritance(final DecorationModel child, final DecorationModel parent) {
151         if (child.getCustom() == null) {
152             child.setCustom(parent.getCustom());
153         } else {
154             child.setCustom(Xpp3Dom.mergeXpp3Dom((Xpp3Dom) child.getCustom(), (Xpp3Dom) parent.getCustom()));
155         }
156     }
157 
158     private void assembleBodyInheritance(
159             final String name,
160             final DecorationModel child,
161             final DecorationModel parent,
162             final URLRebaser urlContainer) {
163         Body cBody = child.getBody();
164         Body pBody = parent.getBody();
165 
166         if (cBody != null || pBody != null) {
167             if (cBody == null) {
168                 cBody = new Body();
169                 child.setBody(cBody);
170             }
171 
172             if (pBody == null) {
173                 pBody = new Body();
174             }
175 
176             if (cBody.getHead() == null && pBody.getHead() != null) {
177                 cBody.setHead(pBody.getHead());
178             }
179 
180             cBody.setLinks(mergeLinkItemLists(cBody.getLinks(), pBody.getLinks(), urlContainer, false));
181 
182             if (cBody.getBreadcrumbs().isEmpty() && !pBody.getBreadcrumbs().isEmpty()) {
183                 LinkItem breadcrumb = new LinkItem();
184                 breadcrumb.setName(name);
185                 breadcrumb.setHref("index.html");
186                 cBody.getBreadcrumbs().add(breadcrumb);
187             }
188             cBody.setBreadcrumbs(
189                     mergeLinkItemLists(cBody.getBreadcrumbs(), pBody.getBreadcrumbs(), urlContainer, true));
190 
191             cBody.setMenus(mergeMenus(cBody.getMenus(), pBody.getMenus(), urlContainer));
192 
193             if (cBody.getFooter() == null && pBody.getFooter() != null) {
194                 cBody.setFooter(pBody.getFooter());
195             }
196         }
197     }
198 
199     private List<Menu> mergeMenus(
200             final List<Menu> childMenus, final List<Menu> parentMenus, final URLRebaser urlContainer) {
201         List<Menu> menus = new ArrayList<Menu>(childMenus.size() + parentMenus.size());
202 
203         for (Menu menu : childMenus) {
204             menus.add(menu);
205         }
206 
207         int topCounter = 0;
208         for (Menu menu : parentMenus) {
209             if ("top".equals(menu.getInherit())) {
210                 final Menu clone = menu.clone();
211 
212                 rebaseMenuPaths(clone.getItems(), urlContainer);
213 
214                 menus.add(topCounter, clone);
215                 topCounter++;
216             } else if ("bottom".equals(menu.getInherit())) {
217                 final Menu clone = menu.clone();
218 
219                 rebaseMenuPaths(clone.getItems(), urlContainer);
220 
221                 menus.add(clone);
222             }
223         }
224 
225         return menus;
226     }
227 
228     private void relativizeMenuPaths(final List<MenuItem> items, final String baseUrl) {
229         for (MenuItem item : items) {
230             relativizeLinkItemPaths(item, baseUrl);
231             relativizeMenuPaths(item.getItems(), baseUrl);
232         }
233     }
234 
235     private void rebaseMenuPaths(final List<MenuItem> items, final URLRebaser urlContainer) {
236         for (MenuItem item : items) {
237             rebaseLinkItemPaths(item, urlContainer);
238             rebaseMenuPaths(item.getItems(), urlContainer);
239         }
240     }
241 
242     private void relativizeLinkItemPaths(final LinkItem item, final String baseUrl) {
243         item.setHref(relativizeLink(item.getHref(), baseUrl));
244     }
245 
246     private void rebaseLinkItemPaths(final LinkItem item, final URLRebaser urlContainer) {
247         item.setHref(urlContainer.rebaseLink(item.getHref()));
248     }
249 
250     private void relativizeLogoPaths(final Logo logo, final String baseUrl) {
251         logo.setImg(relativizeLink(logo.getImg(), baseUrl));
252         relativizeLinkItemPaths(logo, baseUrl);
253     }
254 
255     private void rebaseLogoPaths(final Logo logo, final URLRebaser urlContainer) {
256         logo.setImg(urlContainer.rebaseLink(logo.getImg()));
257         rebaseLinkItemPaths(logo, urlContainer);
258     }
259 
260     private List<LinkItem> mergeLinkItemLists(
261             final List<LinkItem> childList,
262             final List<LinkItem> parentList,
263             final URLRebaser urlContainer,
264             boolean cutParentAfterDuplicate) {
265         List<LinkItem> items = new ArrayList<LinkItem>(childList.size() + parentList.size());
266 
267         for (LinkItem item : parentList) {
268             if (!items.contains(item) && !childList.contains(item)) {
269                 final LinkItem clone = item.clone();
270 
271                 rebaseLinkItemPaths(clone, urlContainer);
272 
273                 items.add(clone);
274             } else if (cutParentAfterDuplicate) {
275                 // if a parent item is found in child, ignore next items (case for breadcrumbs)
276                 // merge ( "B > E", "A > B > C > D" ) -> "A > B > E" (notice missing "C > D")
277                 // see https://issues.apache.org/jira/browse/DOXIASITETOOLS-62
278                 break;
279             }
280         }
281 
282         for (LinkItem item : childList) {
283             if (!items.contains(item)) {
284                 items.add(item);
285             }
286         }
287 
288         return items;
289     }
290 
291     private List<Logo> mergePoweredByLists(
292             final List<Logo> childList, final List<Logo> parentList, final URLRebaser urlContainer) {
293         List<Logo> logos = new ArrayList<Logo>(childList.size() + parentList.size());
294 
295         for (Logo logo : parentList) {
296             if (!logos.contains(logo)) {
297                 final Logo clone = logo.clone();
298 
299                 rebaseLogoPaths(clone, urlContainer);
300 
301                 logos.add(clone);
302             }
303         }
304 
305         for (Logo logo : childList) {
306             if (!logos.contains(logo)) {
307                 logos.add(logo);
308             }
309         }
310 
311         return logos;
312     }
313 
314     // relativize only affects absolute links, if the link has the same scheme, host and port
315     // as the base, it is made into a relative link as viewed from the base
316     private String relativizeLink(final String link, final String baseUri) {
317         if (link == null || baseUri == null) {
318             return link;
319         }
320 
321         // this shouldn't be necessary, just to swallow mal-formed hrefs
322         try {
323             final URIPathDescriptor path = new URIPathDescriptor(baseUri, link);
324 
325             return path.relativizeLink().toString();
326         } catch (IllegalArgumentException e) {
327             return link;
328         }
329     }
330 
331     /**
332      * 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
333      * path.
334      */
335     private static class URLRebaser {
336 
337         private final String oldPath;
338 
339         private final String newPath;
340 
341         /**
342          * Construct a URL rebaser.
343          *
344          * @param oldPath the old path.
345          * @param newPath the new path.
346          */
347         URLRebaser(final String oldPath, final String newPath) {
348             this.oldPath = oldPath;
349             this.newPath = newPath;
350         }
351 
352         /**
353          * Get the new path.
354          *
355          * @return the new path.
356          */
357         public String getNewPath() {
358             return this.newPath;
359         }
360 
361         /**
362          * Get the old path.
363          *
364          * @return the old path.
365          */
366         public String getOldPath() {
367             return this.oldPath;
368         }
369 
370         /**
371          * Rebase only affects relative links, a relative link wrt an old base gets translated,
372          * so it points to the same location as viewed from a new base
373          */
374         public String rebaseLink(final String link) {
375             if (link == null || getOldPath() == null) {
376                 return link;
377             }
378 
379             if (link.contains("${project.")) {
380                 throw new IllegalArgumentException("site.xml late interpolation ${project.*} expression found"
381                         + " in link: '" + link + "'. Use early interpolation ${this.*}");
382             }
383 
384             final URIPathDescriptor oldPath = new URIPathDescriptor(getOldPath(), link);
385 
386             return oldPath.rebaseLink(getNewPath()).toString();
387         }
388     }
389 }