Java tutorial
/* * Copyright (C) 2011 Michael Griffel * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * * This distribution includes other third-party libraries. * These libraries and their corresponding licenses (where different * from the GNU General Public License) are enumerated below. * * PlantUML is a Open-Source tool in Java to draw UML Diagram. * The software is developed by Arnaud Roques at * http://plantuml.sourceforge.org. */ package de.griffel.confluence.plugins.plantuml; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.StringReader; import java.io.StringWriter; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Random; import net.sourceforge.plantuml.BlockUml; import net.sourceforge.plantuml.FileFormat; import net.sourceforge.plantuml.FileFormatOption; import net.sourceforge.plantuml.SourceStringReader; import net.sourceforge.plantuml.core.Diagram; import net.sourceforge.plantuml.core.DiagramType; import net.sourceforge.plantuml.core.ImageData; import net.sourceforge.plantuml.core.UmlSource; import net.sourceforge.plantuml.preproc.Defines; import org.apache.commons.io.HexDump; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringEscapeUtils; import org.apache.commons.lang.time.StopWatch; import org.apache.log4j.Logger; import com.atlassian.confluence.core.ContentEntityObject; import com.atlassian.confluence.importexport.resource.DownloadResourceNotFoundException; import com.atlassian.confluence.importexport.resource.DownloadResourceReader; import com.atlassian.confluence.importexport.resource.DownloadResourceWriter; import com.atlassian.confluence.importexport.resource.UnauthorizedDownloadResourceException; import com.atlassian.confluence.importexport.resource.WritableDownloadResourceManager; import com.atlassian.confluence.pages.Attachment; import com.atlassian.confluence.pages.Page; import com.atlassian.confluence.pages.PageManager; import com.atlassian.confluence.renderer.PageContext; import com.atlassian.confluence.renderer.ShortcutLinkConfig; import com.atlassian.confluence.renderer.ShortcutLinksManager; import com.atlassian.confluence.setup.settings.SettingsManager; import com.atlassian.confluence.spaces.SpaceManager; import com.atlassian.confluence.user.AuthenticatedUserThreadLocal; import com.atlassian.confluence.util.i18n.I18NBeanFactory; import com.atlassian.core.exception.InfrastructureException; import com.atlassian.plugin.PluginAccessor; import com.atlassian.renderer.RenderContext; import com.atlassian.renderer.v2.RenderMode; import com.atlassian.renderer.v2.macro.BaseMacro; import com.atlassian.renderer.v2.macro.MacroException; import de.griffel.confluence.plugins.plantuml.PlantUmlMacro.MySourceStringReader.ImageInfo; import de.griffel.confluence.plugins.plantuml.config.PlantUmlConfiguration; import de.griffel.confluence.plugins.plantuml.config.PlantUmlConfigurationManager; import de.griffel.confluence.plugins.plantuml.preprocess.PageAnchorBuilder; import de.griffel.confluence.plugins.plantuml.preprocess.PlantUmlPreprocessor; import de.griffel.confluence.plugins.plantuml.preprocess.PreprocessingContext; import de.griffel.confluence.plugins.plantuml.preprocess.PreprocessingException; import de.griffel.confluence.plugins.plantuml.preprocess.UmlSourceLocator; import de.griffel.confluence.plugins.plantuml.type.ConfluenceLink; import de.griffel.confluence.plugins.plantuml.type.ImageMap; import de.griffel.confluence.plugins.plantuml.type.UmlSourceBuilder; /** * The Confluence PlantUML Macro. * * @author Michael Griffel */ public class PlantUmlMacro extends BaseMacro { private static final Logger logger = Logger.getLogger(PlantUmlMacro.class); private final WritableDownloadResourceManager writeableDownloadResourceManager; private final PageManager pageManager; private final SpaceManager spaceManager; private final SettingsManager settingsManager; private final PluginAccessor pluginAccessor; private final ShortcutLinksManager shortcutLinksManager; private final PlantUmlConfigurationManager configurationManager; private final I18NBeanFactory i18NBeanFactory; public PlantUmlMacro(WritableDownloadResourceManager writeableDownloadResourceManager, PageManager pageManager, SpaceManager spaceManager, SettingsManager settingsManager, PluginAccessor pluginAccessor, ShortcutLinksManager shortcutLinksManager, PlantUmlConfigurationManager configurationManager, I18NBeanFactory i18NBeanFactory) { this.writeableDownloadResourceManager = writeableDownloadResourceManager; this.pageManager = pageManager; this.spaceManager = spaceManager; this.settingsManager = settingsManager; this.pluginAccessor = pluginAccessor; this.shortcutLinksManager = shortcutLinksManager; this.configurationManager = configurationManager; this.i18NBeanFactory = i18NBeanFactory; } @Override public final boolean isInline() { return false; } public final boolean hasBody() { return true; } public final RenderMode getBodyRenderMode() { return RenderMode.NO_RENDER; } public PageAnchorBuilder createPageAnchorBuilder() { return new PageAnchorBuilder(); } @SuppressWarnings({ "unchecked", "rawtypes" }) public String execute(Map params, final String body, final RenderContext renderContext) throws MacroException { try { final String unescapeHtml = unescapeHtml(body); return executeInternal(params, unescapeHtml, renderContext); } catch (final IOException e) { throw new MacroException(e); } catch (UnauthorizedDownloadResourceException e) { throw new MacroException(e); } catch (DownloadResourceNotFoundException e) { throw new MacroException(e); } } static String unescapeHtml(final String body) throws IOException { final StringWriter sw = new StringWriter(); StringEscapeUtils.unescapeHtml(sw, body); return sw.toString(); } protected final String executeInternal(Map<String, String> params, final String body, final RenderContext renderContext) throws MacroException, IOException, UnauthorizedDownloadResourceException, DownloadResourceNotFoundException { final StopWatch stopWatch = new StopWatch(); stopWatch.start(); final PlantUmlMacroParams macroParams = new PlantUmlMacroParams(params); if (!(renderContext instanceof PageContext)) { throw new MacroException("This macro can only be used in Confluence pages. (ctx=" + renderContext.getClass().getName() + ")"); } final PageContext pageContext = (PageContext) renderContext; final UmlSourceLocator umlSourceLocator = new UmlSourceLocatorConfluence(pageContext); final PreprocessingContext preprocessingContext = new MyPreprocessingContext(pageContext); final DiagramType diagramType = macroParams.getDiagramType(); final boolean dropShadow = macroParams.getDropShadow(); final boolean separation = macroParams.getSeparation(); final PlantUmlConfiguration configuration = configurationManager.load(); final UmlSourceBuilder builder = new UmlSourceBuilder(diagramType, dropShadow, separation, configuration) .append(new StringReader(body)); final PlantUmlPreprocessor preprocessor = new PlantUmlPreprocessor(builder.build(), umlSourceLocator, preprocessingContext); final String umlBlock = preprocessor.toUmlBlock(); final String result = render(umlBlock, pageContext, macroParams, preprocessor); stopWatch.stop(); logger.info(String.format("Rendering %s diagram on page %s:%s took %d ms.", diagramType, pageContext.getSpaceKey(), pageContext.getPageTitle(), stopWatch.getTime())); return result; } private String render(final String umlBlock, final PageContext pageContext, final PlantUmlMacroParams macroParams, final PlantUmlPreprocessor preprocessor) throws IOException, UnauthorizedDownloadResourceException, DownloadResourceNotFoundException { final FileFormat fileFormat = macroParams.getFileFormat(pageContext); final List<String> config = new PlantUmlConfigBuilder().build(macroParams); final MySourceStringReader reader = new MySourceStringReader(new Defines(), umlBlock, config); final StringBuilder sb = new StringBuilder(); if (preprocessor.hasExceptions()) { sb.append("<span class=\"error\">"); for (PreprocessingException exception : preprocessor.getExceptions()) { sb.append("<span class=\"error\">"); sb.append("plantuml: "); sb.append(exception.getDetails()); sb.append("</span><br/>"); } sb.append("</span>"); } while (reader.hasNext()) { final DownloadResourceWriter resourceWriter = writeableDownloadResourceManager.getResourceWriter( AuthenticatedUserThreadLocal.getUsername(), "plantuml", fileFormat.getFileSuffix()); final ImageInfo imageInfo = reader.renderImage(resourceWriter.getStreamForWriting(), fileFormat); final ImageMap cmap = imageInfo.getImageMap(); if (cmap.isValid()) { sb.append(cmap.toHtmlString()); } if (umlBlock.matches(PlantUmlPluginInfo.PLANTUML_VERSION_INFO_REGEX)) { sb.append(new PlantUmlPluginInfo(pluginAccessor, i18NBeanFactory.getI18NBean()).toHtmlString()); } final DownloadResourceInfo resourceInfo; if (macroParams.getExportName() != null && !preprocessor.hasExceptions()) { resourceInfo = attachImage(pageContext.getEntity(), macroParams, imageInfo, fileFormat, resourceWriter); } else { resourceInfo = new DefaultDownloadResourceInfo(writeableDownloadResourceManager, resourceWriter); } if (FileFormat.SVG == fileFormat) { final StringWriter sw = new StringWriter(); IOUtils.copy(resourceInfo.getStreamForReading(), sw); sb.append(sw.getBuffer()); } else /* PNG */ { sb.append("<span class=\"image-wrap\" style=\"").append(macroParams.getAlignment().getCssStyle()) .append("\">"); sb.append("<img"); if (cmap.isValid()) { sb.append(" usemap=\"#"); sb.append(cmap.getId()); sb.append("\""); } sb.append(" src='"); sb.append(resourceInfo.getDownloadPath()); sb.append("'"); sb.append(macroParams.getImageStyle()); sb.append("/>"); sb.append("</span>"); } } if (macroParams.isDebug()) { sb.append("<div class=\"puml-debug\">"); sb.append("<pre>"); final ByteArrayOutputStream baos = new ByteArrayOutputStream(); HexDump.dump(umlBlock.getBytes("UTF-8"), 0, baos, 0); sb.append(baos.toString()); // HexDump class writer bytes with JVM default encoding sb.append("</pre>"); sb.append("</div>"); } return sb.toString(); } private DownloadResourceInfo attachImage(final ContentEntityObject page, final PlantUmlMacroParams macroParams, ImageInfo imageInfo, final FileFormat fileFormat, final DownloadResourceWriter resourceWriter) throws UnauthorizedDownloadResourceException, DownloadResourceNotFoundException, IOException { final String attachmentName; if (imageInfo.isSplitImage()) { attachmentName = macroParams.getExportName() + "-" + imageInfo.getIndex() + fileFormat.getFileSuffix(); } else { attachmentName = macroParams.getExportName() + fileFormat.getFileSuffix(); } Attachment attachment = pageManager.getAttachmentManager().getAttachment(page, attachmentName); final Attachment previousVersion; if (attachment == null) { previousVersion = null; attachment = new Attachment(); attachment.setFileName(attachmentName); attachment.setContentType("image/" + fileFormat.name().toLowerCase()); attachment.setComment("PlantUML Diagram (generated)"); } else { try { previousVersion = (Attachment) attachment.clone(); } catch (CloneNotSupportedException e) { throw new InfrastructureException(e); } } final DownloadResourceReader resourceReader = writeableDownloadResourceManager.getResourceReader( AuthenticatedUserThreadLocal.getUsername(), resourceWriter.getResourcePath(), Collections.emptyMap()); if (previousVersion == null || previousVersion.getFileSize() != resourceReader.getContentLength() || !IOUtils.contentEquals(previousVersion.getContentsAsStream(), resourceReader.getStreamForReading())) { attachment.setFileSize(resourceReader.getContentLength()); page.addAttachment(attachment); pageManager.getAttachmentManager().saveAttachment(attachment, previousVersion, resourceReader.getStreamForReading()); logger.debug("Saved image as attachment " + attachmentName); } return new AttachmentDownloadResourceInfo(settingsManager.getGlobalSettings().getBaseUrl(), attachment); } private final class MyPreprocessingContext implements PreprocessingContext { private final PageContext pageContext; /** * {@inheritDoc} */ private MyPreprocessingContext(PageContext pageContext) { this.pageContext = pageContext; } /** * Returns the base URL from the global settings. * * @return the base URL from the global settings. */ public String getBaseUrl() { final String baseUrl = settingsManager.getGlobalSettings().getBaseUrl(); return baseUrl; } /** * {@inheritDoc} */ public PageContext getPageContext() { return pageContext; } public SpaceManager getSpaceManager() { return spaceManager; } /** * {@inheritDoc} */ public PageManager getPageManager() { return pageManager; } /** * {@inheritDoc} */ public Map<String, ShortcutLinkConfig> getShortcutLinks() { return shortcutLinksManager.getShortcutLinks(); } /** * {@inheritDoc} */ public PageAnchorBuilder getPageAnchorBuilder() { return createPageAnchorBuilder(); } } /** * Gets the UML source either from a Confluence page or from an attachment. */ private final class UmlSourceLocatorConfluence implements UmlSourceLocator { private final PageContext pageContext; /** * @param pageContext */ private UmlSourceLocatorConfluence(PageContext pageContext) { this.pageContext = pageContext; } public UmlSource get(String name) throws IOException { final ConfluenceLink.Parser parser = new ConfluenceLink.Parser(pageContext, spaceManager, pageManager); final ConfluenceLink confluenceLink = parser.parse(name); if (logger.isDebugEnabled()) { logger.debug("Link '" + name + "' -> " + confluenceLink); } final Page page = pageManager.getPage(confluenceLink.getSpaceKey(), confluenceLink.getPageTitle()); // page cannot be null since it is validated before if (confluenceLink.hasAttachmentName()) { final Attachment attachment = pageManager.getAttachmentManager().getAttachment(page, confluenceLink.getAttachmentName()); if (attachment == null) { throw new IOException("Cannot find attachment '" + confluenceLink.getAttachmentName() + "' on page '" + confluenceLink.getPageTitle() + "' in space '" + confluenceLink.getSpaceKey() + "'"); } return new UmlSourceBuilder().append(attachment.getContentsAsStream()).build(); } else { return new UmlSourceBuilder().append(page.getBodyAsStringWithoutMarkup()).build(); } } } /** * Extension to {@link SourceStringReader} to add the function to get the image map for the diagram. */ public static class MySourceStringReader extends SourceStringReader { private static final Random RANDOM = new Random(); private final BlockUml blockUml; private final Diagram system; private int index = 0; /** * Creates extended version of {@link SourceStringReader}. * * @param defines defines * @param source plant uml source * @param config configuration */ public MySourceStringReader(Defines defines, String source, List<String> config) { super(defines, source, config); blockUml = getBlocks().iterator().next(); system = blockUml.getDiagram(); } public boolean hasNext() { return index < system.getNbImages(); } public final ImageInfo renderImage(OutputStream outputStream, FileFormat format) throws IOException { final ImageData imageData = system.exportDiagram(outputStream, index, new FileFormatOption(format)); final ImageMap imageMap; if (imageData.containsCMapData()) { final String randomId = String.valueOf(Math.abs(RANDOM.nextInt())); imageMap = new ImageMap(imageData.getCMapData("plantuml" + randomId)); } else { imageMap = ImageMap.NULL; } return new ImageInfo(imageMap, index++); } public final class ImageInfo { private final ImageMap imageMap; private final int index; ImageInfo(ImageMap imageMap, int index) { this.imageMap = imageMap; this.index = index; } public boolean isSplitImage() { return system.getNbImages() > 1; } public ImageMap getImageMap() { return imageMap; } public int getIndex() { return index; } } } }