1   /**
2    * Copyright (c) 2000-2009 Liferay, Inc. All rights reserved.
3    *
4    *
5    *
6    *
7    * The contents of this file are subject to the terms of the Liferay Enterprise
8    * Subscription License ("License"). You may not use this file except in
9    * compliance with the License. You can obtain a copy of the License by
10   * contacting Liferay, Inc. See the License for the specific language governing
11   * permissions and limitations under the License, including but not limited to
12   * distribution rights of the Software.
13   *
14   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20   * SOFTWARE.
21   */
22  
23  package com.liferay.portal.lar;
24  
25  import com.liferay.portal.PortalException;
26  import com.liferay.portal.SystemException;
27  import com.liferay.portal.kernel.io.FileCacheOutputStream;
28  import com.liferay.portal.kernel.log.Log;
29  import com.liferay.portal.kernel.log.LogFactoryUtil;
30  import com.liferay.portal.kernel.util.FileUtil;
31  import com.liferay.portal.kernel.util.MapUtil;
32  import com.liferay.portal.kernel.util.ReleaseInfo;
33  import com.liferay.portal.kernel.util.StringPool;
34  import com.liferay.portal.kernel.util.StringUtil;
35  import com.liferay.portal.kernel.util.Time;
36  import com.liferay.portal.kernel.xml.Document;
37  import com.liferay.portal.kernel.xml.Element;
38  import com.liferay.portal.kernel.xml.SAXReaderUtil;
39  import com.liferay.portal.kernel.zip.ZipWriter;
40  import com.liferay.portal.model.Group;
41  import com.liferay.portal.model.GroupConstants;
42  import com.liferay.portal.model.Image;
43  import com.liferay.portal.model.Layout;
44  import com.liferay.portal.model.LayoutConstants;
45  import com.liferay.portal.model.LayoutSet;
46  import com.liferay.portal.model.LayoutTypePortlet;
47  import com.liferay.portal.model.Portlet;
48  import com.liferay.portal.model.PortletConstants;
49  import com.liferay.portal.model.Resource;
50  import com.liferay.portal.model.ResourceConstants;
51  import com.liferay.portal.model.Theme;
52  import com.liferay.portal.service.GroupLocalServiceUtil;
53  import com.liferay.portal.service.ImageLocalServiceUtil;
54  import com.liferay.portal.service.LayoutLocalServiceUtil;
55  import com.liferay.portal.service.LayoutSetLocalServiceUtil;
56  import com.liferay.portal.service.PortletLocalServiceUtil;
57  import com.liferay.portal.service.UserLocalServiceUtil;
58  import com.liferay.portal.service.permission.PortletPermissionUtil;
59  import com.liferay.portal.service.persistence.LayoutUtil;
60  import com.liferay.portal.theme.ThemeLoader;
61  import com.liferay.portal.theme.ThemeLoaderFactory;
62  import com.liferay.portal.util.ContentUtil;
63  import com.liferay.portal.util.PortletKeys;
64  import com.liferay.portal.util.PropsValues;
65  import com.liferay.portal.velocity.VelocityContextPool;
66  
67  import java.io.File;
68  import java.io.IOException;
69  
70  import java.util.Date;
71  import java.util.HashSet;
72  import java.util.LinkedHashMap;
73  import java.util.List;
74  import java.util.Map;
75  
76  import javax.servlet.ServletContext;
77  
78  import org.apache.commons.lang.time.StopWatch;
79  
80  /**
81   * <a href="LayoutExporter.java.html"><b><i>View Source</i></b></a>
82   *
83   * @author Brian Wing Shun Chan
84   * @author Joel Kozikowski
85   * @author Charles May
86   * @author Raymond Augé
87   * @author Jorge Ferrer
88   * @author Bruno Farache
89   */
90  public class LayoutExporter {
91  
92      public byte[] exportLayouts(
93              long groupId, boolean privateLayout, long[] layoutIds,
94              Map<String, String[]> parameterMap, Date startDate, Date endDate)
95          throws PortalException, SystemException {
96  
97          FileCacheOutputStream fcos = exportLayoutsAsStream(
98              groupId, privateLayout, layoutIds, parameterMap, startDate,
99              endDate);
100 
101         try {
102             return fcos.getBytes();
103         }
104         catch (IOException ioe) {
105             throw new SystemException(ioe);
106         }
107         finally {
108             fcos.cleanUp();
109         }
110     }
111 
112     public FileCacheOutputStream exportLayoutsAsStream(
113             long groupId, boolean privateLayout, long[] layoutIds,
114             Map<String, String[]> parameterMap, Date startDate, Date endDate)
115         throws PortalException, SystemException {
116 
117         boolean exportPermissions = MapUtil.getBoolean(
118             parameterMap, PortletDataHandlerKeys.PERMISSIONS);
119         boolean exportUserPermissions = MapUtil.getBoolean(
120             parameterMap, PortletDataHandlerKeys.USER_PERMISSIONS);
121         boolean exportPortletArchivedSetups = MapUtil.getBoolean(
122             parameterMap, PortletDataHandlerKeys.PORTLET_ARCHIVED_SETUPS);
123         boolean exportPortletUserPreferences = MapUtil.getBoolean(
124             parameterMap, PortletDataHandlerKeys.PORTLET_USER_PREFERENCES);
125         boolean exportTheme = MapUtil.getBoolean(
126             parameterMap, PortletDataHandlerKeys.THEME);
127 
128         if (_log.isDebugEnabled()) {
129             _log.debug("Export permissions " + exportPermissions);
130             _log.debug("Export user permissions " + exportUserPermissions);
131             _log.debug(
132                 "Export portlet archived setups " +
133                     exportPortletArchivedSetups);
134             _log.debug(
135                 "Export portlet user preferences " +
136                     exportPortletUserPreferences);
137             _log.debug("Export theme " + exportTheme);
138         }
139 
140         StopWatch stopWatch = null;
141 
142         if (_log.isInfoEnabled()) {
143             stopWatch = new StopWatch();
144 
145             stopWatch.start();
146         }
147 
148         LayoutCache layoutCache = new LayoutCache();
149 
150         LayoutSet layoutSet = LayoutSetLocalServiceUtil.getLayoutSet(
151             groupId, privateLayout);
152 
153         long companyId = layoutSet.getCompanyId();
154         long defaultUserId = UserLocalServiceUtil.getDefaultUserId(companyId);
155 
156         ZipWriter zipWriter = null;
157 
158         try {
159             zipWriter = new ZipWriter();
160         }
161         catch (IOException ioe) {
162             throw new SystemException(ioe);
163         }
164 
165         PortletDataContext context = new PortletDataContextImpl(
166             companyId, groupId, parameterMap, new HashSet<String>(), startDate,
167             endDate, zipWriter);
168 
169         Group guestGroup = GroupLocalServiceUtil.getGroup(
170             companyId, GroupConstants.GUEST);
171 
172         // Build compatibility
173 
174         Document doc = SAXReaderUtil.createDocument();
175 
176         Element root = doc.addElement("root");
177 
178         Element header = root.addElement("header");
179 
180         header.addAttribute(
181             "build-number", String.valueOf(ReleaseInfo.getBuildNumber()));
182         header.addAttribute("export-date", Time.getRFC822());
183 
184         if (context.hasDateRange()) {
185             header.addAttribute(
186                 "start-date", String.valueOf(context.getStartDate()));
187             header.addAttribute(
188                 "end-date", String.valueOf(context.getEndDate()));
189         }
190 
191         header.addAttribute("type", "layout-set");
192         header.addAttribute("group-id", String.valueOf(groupId));
193         header.addAttribute("private-layout", String.valueOf(privateLayout));
194         header.addAttribute("theme-id", layoutSet.getThemeId());
195         header.addAttribute("color-scheme-id", layoutSet.getColorSchemeId());
196 
197         // Layout Configuration Portlet
198 
199         Portlet layoutConfigurationPortlet =
200             PortletLocalServiceUtil.getPortletById(
201                 context.getCompanyId(), PortletKeys.LAYOUT_CONFIGURATION);
202 
203         // Layouts
204 
205         Map<String, Object[]> portletIds =
206             new LinkedHashMap<String, Object[]>();
207 
208         List<Layout> layouts = null;
209 
210         if ((layoutIds == null) || (layoutIds.length == 0)) {
211             layouts = LayoutLocalServiceUtil.getLayouts(groupId, privateLayout);
212         }
213         else {
214             layouts = LayoutLocalServiceUtil.getLayouts(
215                 groupId, privateLayout, layoutIds);
216         }
217 
218         Element layoutsEl = root.addElement("layouts");
219 
220         for (Layout layout : layouts) {
221             context.setPlid(layout.getPlid());
222 
223             Document layoutDoc = SAXReaderUtil.createDocument();
224 
225             Element layoutEl = layoutDoc.addElement("layout");
226 
227             layoutEl.addAttribute("old-plid", String.valueOf(layout.getPlid()));
228             layoutEl.addAttribute(
229                 "layout-id", String.valueOf(layout.getLayoutId()));
230             layoutEl.addElement("parent-layout-id").addText(
231                 String.valueOf(layout.getParentLayoutId()));
232             layoutEl.addElement("name").addCDATA(layout.getName());
233             layoutEl.addElement("title").addCDATA(layout.getTitle());
234             layoutEl.addElement("description").addText(layout.getDescription());
235             layoutEl.addElement("type").addText(layout.getType());
236             layoutEl.addElement("type-settings").addCDATA(
237                 layout.getTypeSettings());
238             layoutEl.addElement("hidden").addText(
239                 String.valueOf(layout.getHidden()));
240             layoutEl.addElement("friendly-url").addText(
241                 layout.getFriendlyURL());
242             layoutEl.addElement("icon-image").addText(
243                 String.valueOf(layout.getIconImage()));
244 
245             if (layout.isIconImage()) {
246                 Image image = ImageLocalServiceUtil.getImage(
247                     layout.getIconImageId());
248 
249                 if (image != null) {
250                     String iconPath = getLayoutIconPath(context, layout, image);
251 
252                     layoutEl.addElement("icon-image-path").addText(
253                         iconPath);
254 
255                     context.addZipEntry(iconPath, image.getTextObj());
256                 }
257             }
258 
259             layoutEl.addElement("theme-id").addText(layout.getThemeId());
260             layoutEl.addElement("color-scheme-id").addText(
261                 layout.getColorSchemeId());
262             layoutEl.addElement("wap-theme-id").addText(layout.getWapThemeId());
263             layoutEl.addElement("wap-color-scheme-id").addText(
264                 layout.getWapColorSchemeId());
265             layoutEl.addElement("css").addCDATA(layout.getCss());
266             layoutEl.addElement("priority").addText(
267                 String.valueOf(layout.getPriority()));
268 
269             // Layout permissions
270 
271             if (exportPermissions) {
272                 Element permissionsEl = layoutEl.addElement("permissions");
273 
274                 String resourceName = Layout.class.getName();
275                 String resourcePrimKey = String.valueOf(layout.getPlid());
276 
277                 if (PropsValues.PERMISSIONS_USER_CHECK_ALGORITHM == 5) {
278                     exportLayoutPermissions_5(
279                         layoutCache, companyId, groupId, resourceName,
280                         resourcePrimKey, permissionsEl);
281                 }
282                 else if (PropsValues.PERMISSIONS_USER_CHECK_ALGORITHM == 6) {
283                     exportLayoutPermissions_6(
284                         layoutCache, companyId, groupId, resourceName,
285                         resourcePrimKey, permissionsEl);
286                 }
287                 else {
288                     exportLayoutPermissions_4(
289                         layoutCache, companyId, groupId, guestGroup,
290                         resourceName, resourcePrimKey, permissionsEl,
291                         exportUserPermissions);
292                 }
293             }
294 
295             if (layout.getType().equals(LayoutConstants.TYPE_PORTLET)) {
296                 LayoutTypePortlet layoutTypePortlet =
297                     (LayoutTypePortlet)layout.getLayoutType();
298 
299                 for (String portletId : layoutTypePortlet.getPortletIds()) {
300                     String key = PortletPermissionUtil.getPrimaryKey(
301                         layout.getPlid(), portletId);
302 
303                     portletIds.put(
304                         key, new Object[] {portletId, layout.getPlid()});
305                 }
306             }
307 
308             String layoutPath = context.getLayoutPath(layout.getLayoutId()) +
309                 "/layout.xml";
310 
311             Element el = layoutsEl.addElement("layout");
312 
313             el.addAttribute("layout-id", String.valueOf(layout.getLayoutId()));
314             el.addAttribute("path", layoutPath);
315 
316             _portletExporter.exportPortletData(
317                 context, layoutConfigurationPortlet, layout, null, layoutEl);
318 
319             try {
320                 context.addZipEntry(layoutPath, layoutDoc.formattedString());
321             }
322             catch (IOException ioe) {
323             }
324         }
325 
326         if (PropsValues.PERMISSIONS_USER_CHECK_ALGORITHM < 5) {
327             Element rolesEl = root.addElement("roles");
328 
329             // Layout roles
330 
331             if (exportPermissions) {
332                 exportLayoutRoles(layoutCache, companyId, groupId, rolesEl);
333             }
334         }
335 
336         // Export Portlets
337 
338         Element portletsEl = root.addElement("portlets");
339 
340         for (Map.Entry<String, Object[]> portletIdsEntry :
341                 portletIds.entrySet()) {
342 
343             String portletId = (String)portletIdsEntry.getValue()[0];
344             long plid = (Long)portletIdsEntry.getValue()[1];
345 
346             Layout layout = LayoutUtil.findByPrimaryKey(plid);
347 
348             context.setPlid(layout.getPlid());
349             context.setOldPlid(layout.getPlid());
350 
351             boolean[] exportPortletControls = getExportPortletControls(
352                 context.getCompanyId(), portletId, context, parameterMap);
353 
354             _portletExporter.exportPortlet(
355                 context, layoutCache, portletId, layout, portletsEl,
356                 defaultUserId, exportPermissions, exportPortletArchivedSetups,
357                 exportPortletControls[0], exportPortletControls[1],
358                 exportPortletUserPreferences, exportUserPermissions);
359         }
360 
361         // Comments
362 
363         _portletExporter.exportComments(context, root);
364 
365         // Ratings
366 
367         _portletExporter.exportRatings(context, root);
368 
369         // Tags
370 
371         _portletExporter.exportTags(context, root);
372 
373         // Look and feel
374 
375         FileCacheOutputStream fcos = null;
376 
377         try {
378             if (exportTheme) {
379                 fcos = exportTheme(layoutSet);
380             }
381 
382             // Log
383 
384             if (_log.isInfoEnabled()) {
385                 _log.info(
386                     "Exporting layouts takes " + stopWatch.getTime() + " ms");
387             }
388 
389             // Zip
390 
391             context.addZipEntry("/manifest.xml", doc.formattedString());
392 
393             if (fcos != null) {
394                 context.addZipEntry("/theme.zip", fcos.getFileInputStream());
395             }
396 
397             return zipWriter.finishWithStream();
398         }
399         catch (IOException ioe) {
400             throw new SystemException(ioe);
401         }
402         finally {
403             if (fcos != null) {
404                 fcos.cleanUp();
405             }
406         }
407     }
408 
409     protected void exportLayoutPermissions_4(
410             LayoutCache layoutCache, long companyId, long groupId,
411             Group guestGroup, String resourceName, String resourcePrimKey,
412             Element permissionsEl, boolean exportUserPermissions)
413         throws SystemException {
414 
415         _portletExporter.exportGroupPermissions(
416             companyId, groupId, resourceName, resourcePrimKey, permissionsEl,
417             "community-actions");
418 
419         if (groupId != guestGroup.getGroupId()) {
420             _portletExporter.exportGroupPermissions(
421                 companyId, guestGroup.getGroupId(), resourceName,
422                 resourcePrimKey, permissionsEl, "guest-actions");
423         }
424 
425         if (exportUserPermissions) {
426             _portletExporter.exportUserPermissions(
427                 layoutCache, companyId, groupId, resourceName, resourcePrimKey,
428                 permissionsEl);
429         }
430 
431         _portletExporter.exportInheritedPermissions(
432             layoutCache, companyId, resourceName, resourcePrimKey,
433             permissionsEl, "organization");
434 
435         _portletExporter.exportInheritedPermissions(
436             layoutCache, companyId, resourceName, resourcePrimKey,
437             permissionsEl, "location");
438 
439         _portletExporter.exportInheritedPermissions(
440             layoutCache, companyId, resourceName, resourcePrimKey,
441             permissionsEl, "user-group");
442     }
443 
444     protected void exportLayoutPermissions_5(
445             LayoutCache layoutCache, long companyId, long groupId,
446             String resourceName, String resourcePrimKey, Element permissionsEl)
447         throws PortalException, SystemException {
448 
449         boolean portletActions = false;
450 
451         Resource resource = layoutCache.getResource(
452             companyId, groupId, resourceName,
453             ResourceConstants.SCOPE_INDIVIDUAL, resourcePrimKey,
454             portletActions);
455 
456         _portletExporter.exportPermissions_5(
457             layoutCache, groupId, resourceName, resource.getResourceId(),
458             permissionsEl);
459     }
460 
461     protected void exportLayoutPermissions_6(
462             LayoutCache layoutCache, long companyId, long groupId,
463             String resourceName, String resourcePrimKey, Element permissionsEl)
464         throws PortalException, SystemException {
465 
466         boolean portletActions = false;
467 
468         _portletExporter.exportPermissions_6(
469             layoutCache, companyId, groupId, resourceName, resourcePrimKey,
470             permissionsEl, portletActions);
471     }
472 
473     protected void exportLayoutRoles(
474             LayoutCache layoutCache, long companyId, long groupId,
475             Element rolesEl)
476         throws SystemException {
477 
478         String resourceName = Layout.class.getName();
479 
480         _portletExporter.exportGroupRoles(
481             layoutCache, companyId, groupId, resourceName, "community",
482             rolesEl);
483 
484         _portletExporter.exportUserRoles(
485         layoutCache, companyId, groupId, resourceName, rolesEl);
486 
487         _portletExporter.exportInheritedRoles(
488             layoutCache, companyId, groupId, resourceName, "organization",
489             rolesEl);
490 
491         _portletExporter.exportInheritedRoles(
492             layoutCache, companyId, groupId, resourceName, "location", rolesEl);
493 
494         _portletExporter.exportInheritedRoles(
495             layoutCache, companyId, groupId, resourceName, "user-group",
496             rolesEl);
497     }
498 
499     protected FileCacheOutputStream exportTheme(LayoutSet layoutSet)
500         throws IOException {
501 
502         Theme theme = layoutSet.getTheme();
503 
504         ZipWriter zipWriter = new ZipWriter();
505 
506         String lookAndFeelXML = ContentUtil.get(
507             "com/liferay/portal/dependencies/liferay-look-and-feel.xml.tmpl");
508 
509         lookAndFeelXML = StringUtil.replace(
510             lookAndFeelXML,
511             new String[] {
512                 "[$TEMPLATE_EXTENSION$]", "[$VIRTUAL_PATH$]"
513             },
514             new String[] {
515                 theme.getTemplateExtension(), theme.getVirtualPath()
516             }
517         );
518 
519         zipWriter.addEntry("liferay-look-and-feel.xml", lookAndFeelXML);
520 
521         String servletContextName = theme.getServletContextName();
522 
523         ServletContext servletContext = VelocityContextPool.get(
524             servletContextName);
525 
526         if (servletContext == null) {
527             if (_log.isWarnEnabled()) {
528                 _log.warn(
529                     "Servlet context not found for theme " +
530                         theme.getThemeId());
531             }
532 
533             return null;
534         }
535 
536         File cssPath = null;
537         File imagesPath = null;
538         File javaScriptPath = null;
539         File templatesPath = null;
540 
541         if (!theme.isLoadFromServletContext()) {
542             ThemeLoader themeLoader = ThemeLoaderFactory.getThemeLoader(
543                 servletContextName);
544 
545             if (themeLoader == null) {
546                 _log.error(
547                     servletContextName + " does not map to a theme loader");
548             }
549             else {
550                 String realPath =
551                     themeLoader.getFileStorage().getPath() + "/" +
552                         theme.getName();
553 
554                 cssPath = new File(realPath + "/css");
555                 imagesPath = new File(realPath + "/images");
556                 javaScriptPath = new File(realPath + "/javascript");
557                 templatesPath = new File(realPath + "/templates");
558             }
559         }
560         else {
561             cssPath = new File(servletContext.getRealPath(theme.getCssPath()));
562             imagesPath = new File(
563                 servletContext.getRealPath(theme.getImagesPath()));
564             javaScriptPath = new File(
565                 servletContext.getRealPath(theme.getJavaScriptPath()));
566             templatesPath = new File(
567                 servletContext.getRealPath(theme.getTemplatesPath()));
568         }
569 
570         exportThemeFiles("css", cssPath, zipWriter);
571         exportThemeFiles("images", imagesPath, zipWriter);
572         exportThemeFiles("javascript", javaScriptPath, zipWriter);
573         exportThemeFiles("templates", templatesPath, zipWriter);
574 
575         return zipWriter.finishWithStream();
576     }
577 
578     protected void exportThemeFiles(String path, File dir, ZipWriter zipWriter)
579         throws IOException {
580 
581         if ((dir == null) || (!dir.exists())) {
582             return;
583         }
584 
585         File[] files = dir.listFiles();
586 
587         for (int i = 0; i < files.length; i++) {
588             File file = files[i];
589 
590             if (file.isDirectory()) {
591                 exportThemeFiles(path + "/" + file.getName(), file, zipWriter);
592             }
593             else {
594                 zipWriter.addEntry(
595                     path + "/" + file.getName(), FileUtil.getBytes(file));
596             }
597         }
598     }
599 
600     protected boolean[] getExportPortletControls(
601             long companyId, String portletId, PortletDataContext context,
602             Map<String, String[]> parameterMap)
603         throws SystemException {
604 
605         boolean exportPortletData = MapUtil.getBoolean(
606             parameterMap, PortletDataHandlerKeys.PORTLET_DATA);
607         boolean exportPortletDataAll = MapUtil.getBoolean(
608             parameterMap, PortletDataHandlerKeys.PORTLET_DATA_ALL);
609         boolean exportPortletSetup = MapUtil.getBoolean(
610             parameterMap, PortletDataHandlerKeys.PORTLET_SETUP);
611 
612         if (_log.isDebugEnabled()) {
613             _log.debug("Export portlet data " + exportPortletData);
614             _log.debug("Export all portlet data " + exportPortletDataAll);
615             _log.debug("Export portlet setup " + exportPortletSetup);
616         }
617 
618         boolean exportCurPortletData = exportPortletData;
619         boolean exportCurPortletSetup = exportPortletSetup;
620 
621         if (exportPortletDataAll) {
622             exportCurPortletData = true;
623             exportCurPortletSetup = true;
624         }
625         else {
626             Portlet portlet = PortletLocalServiceUtil.getPortletById(
627                 companyId, portletId);
628 
629             if (portlet != null) {
630                 String portletDataHandlerClass =
631                     portlet.getPortletDataHandlerClass();
632 
633                 if (portletDataHandlerClass != null) {
634                     String rootPortletId = PortletConstants.getRootPortletId(
635                         portletId);
636 
637                     exportCurPortletData =
638                         exportPortletData &&
639                         MapUtil.getBoolean(
640                             parameterMap,
641                             PortletDataHandlerKeys.PORTLET_DATA +
642                                 StringPool.UNDERLINE + rootPortletId);
643 
644                     exportCurPortletSetup =
645                         exportPortletData &&
646                         MapUtil.getBoolean(
647                             parameterMap,
648                             PortletDataHandlerKeys.PORTLET_SETUP +
649                                 StringPool.UNDERLINE + rootPortletId);
650                 }
651             }
652         }
653 
654         return new boolean[] {exportCurPortletData, exportCurPortletSetup};
655     }
656 
657     protected String getLayoutIconPath(
658         PortletDataContext context, Layout layout, Image image) {
659 
660         StringBuilder sb = new StringBuilder();
661 
662         sb.append(context.getLayoutPath(layout.getLayoutId()));
663         sb.append("/icons/");
664         sb.append(image.getImageId());
665         sb.append(StringPool.PERIOD);
666         sb.append(image.getType());
667 
668         return sb.toString();
669     }
670 
671     private static Log _log = LogFactoryUtil.getLog(LayoutExporter.class);
672 
673     private PortletExporter _portletExporter = new PortletExporter();
674 
675 }