org.xwiki.rendering.internal.wiki.XWikiWikiModel.java Source code

Java tutorial

Introduction

Here is the source code for org.xwiki.rendering.internal.wiki.XWikiWikiModel.java

Source

/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.xwiki.rendering.internal.wiki;

import java.io.IOException;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.List;
import java.util.Map;

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;

import org.apache.commons.lang3.StringUtils;
import org.w3c.css.sac.InputSource;
import org.w3c.dom.css.CSSStyleDeclaration;
import org.xwiki.bridge.DocumentAccessBridge;
import org.xwiki.bridge.SkinAccessBridge;
import org.xwiki.component.annotation.Component;
import org.xwiki.model.reference.AttachmentReference;
import org.xwiki.model.reference.AttachmentReferenceResolver;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.model.reference.DocumentReferenceResolver;
import org.xwiki.model.reference.EntityReferenceSerializer;
import org.xwiki.rendering.internal.configuration.XWikiRenderingConfiguration;
import org.xwiki.rendering.listener.reference.AttachmentResourceReference;
import org.xwiki.rendering.listener.reference.DocumentResourceReference;
import org.xwiki.rendering.listener.reference.ResourceReference;
import org.xwiki.rendering.listener.reference.ResourceType;
import org.xwiki.rendering.wiki.WikiModel;

import com.steadystate.css.parser.CSSOMParser;
import com.steadystate.css.parser.SACParserCSS21;

/**
 * Implementation using the Document Access Bridge ({@link DocumentAccessBridge}).
 * 
 * @version $Id: 98254e2dea97b13d764f7a73042f2214306fb067 $
 * @since 2.0M1
 */
@Component
@Singleton
public class XWikiWikiModel implements WikiModel {
    /**
     * The suffix used to mark an amount of pixels.
     */
    private static final String PIXELS = "px";

    /**
     * The name of the {@code width} image parameter.
     */
    private static final String WIDTH = "width";

    /**
     * The name of the {@code height} image parameter.
     */
    private static final String HEIGHT = "height";

    /**
     * The component used to access configuration parameters.
     */
    @Inject
    private XWikiRenderingConfiguration xwikiRenderingConfiguration;

    /**
     * The component used to access the underlying XWiki model.
     */
    @Inject
    private DocumentAccessBridge documentAccessBridge;

    /**
     * Used to find the URL for an icon.
     */
    @Inject
    private SkinAccessBridge skinAccessBridge;

    /**
     * The component used to serialize entity references to strings.
     */
    @Inject
    @Named("compactwiki")
    private EntityReferenceSerializer<String> compactEntityReferenceSerializer;

    /**
     * Convert an Attachment Reference from a String into an Attachment object.
     */
    @Inject
    @Named("current")
    private AttachmentReferenceResolver<String> currentAttachmentReferenceResolver;

    /**
     * Used to resolve a Resource Reference into a proper Document Reference.
     */
    @Inject
    @Named("current")
    private DocumentReferenceResolver<String> currentDocumentReferenceResolver;

    /**
     * The object used to parse the CSS from the image style parameter.
     * <p>
     * NOTE: We explicitly pass the CSS SAC parser because otherwise (e.g. using the default constructor)
     * {@link CSSOMParser} sets the {@code org.w3c.css.sac.parser} system property to its own implementation, i.e.
     * {@link com.steadystate.css.parser.SACParserCSS2}, affecting other components that require a CSS SAC parser (e.g.
     * PDF export).
     * 
     * @see <a href="http://jira.xwiki.org/jira/browse/XWIKI-5625">XWIKI-5625: PDF styling doesn't work anymore</a>
     */
    private final CSSOMParser cssParser = new CSSOMParser(new SACParserCSS21());

    /**
     * {@inheritDoc}
     * @since 2.5RC1
     */
    @Override
    public String getLinkURL(ResourceReference linkReference) {
        return this.documentAccessBridge.getAttachmentURL(resolveAttachmentReference(linkReference),
                linkReference.getParameter(AttachmentResourceReference.QUERY_STRING), true);
    }

    /**
     * {@inheritDoc}
     * @since 2.5RC1
     */
    public String getImageURL(ResourceReference imageReference, Map<String, String> parameters) {
        // Handle icon references
        if (imageReference.getType().equals(ResourceType.ICON)) {
            return this.skinAccessBridge.getIconURL(imageReference.getReference());
        }

        // Handle attachment references
        String url = getLinkURL(imageReference);
        if (!this.xwikiRenderingConfiguration.isImageDimensionsIncludedInImageURL()) {
            return url;
        }

        StringBuilder queryString = getImageURLQueryString(parameters);
        if (queryString.length() == 0) {
            return url;
        }

        // Determine the insertion point.
        int insertionPoint = url.lastIndexOf('#');
        if (insertionPoint < 0) {
            // No fragment identifier.
            insertionPoint = url.length();
        }
        if (url.lastIndexOf('?', insertionPoint) < 0) {
            // No query string.
            queryString.setCharAt(0, '?');
        }

        // Insert the query string.
        return new StringBuilder(url).insert(insertionPoint, queryString).toString();
    }

    @Override
    public boolean isDocumentAvailable(ResourceReference documentResourceReference) {
        DocumentReference documentReference = resolveDocumentReference(documentResourceReference);
        return this.documentAccessBridge.exists(documentReference);
    }

    @Override
    public String getDocumentViewURL(ResourceReference documentResourceReference) {
        DocumentReference documentReference = resolveDocumentReference(documentResourceReference);
        return this.documentAccessBridge.getDocumentURL(documentReference, "view",
                documentResourceReference.getParameter(DocumentResourceReference.QUERY_STRING),
                documentResourceReference.getParameter(DocumentResourceReference.ANCHOR));
    }

    @Override
    public String getDocumentEditURL(ResourceReference documentResourceReference) {
        // Add the parent=<current document name> parameter to the query string of the edit URL so that
        // the new document is created with the current page as its parent.
        String modifiedQueryString = documentResourceReference.getParameter(DocumentResourceReference.QUERY_STRING);
        if (StringUtils.isBlank(modifiedQueryString)) {
            DocumentReference reference = this.documentAccessBridge.getCurrentDocumentReference();
            if (reference != null) {
                try {
                    // Note 1: we encode using UTF8 since it's the W3C recommendation.
                    // See http://www.w3.org/TR/html40/appendix/notes.html#non-ascii-chars

                    // Note 2: We need to be careful to use a compact serializer so that the wiki part is not
                    // part of the generated String so that when the user clicks on the link, the new page is created
                    // with a relative parent (and thus the new page can be moved from one wiki to another easily
                    // without having to change the parent reference).

                    // TODO: Once the xwiki-url module is usable, refactor this code to use it and remove the need to
                    // perform explicit encoding here.

                    modifiedQueryString = "parent=" + URLEncoder
                            .encode(this.compactEntityReferenceSerializer.serialize(reference), "UTF-8");
                } catch (UnsupportedEncodingException e) {
                    // Not supporting UTF-8 as a valid encoding for some reasons. We consider XWiki cannot work
                    // without that encoding.
                    throw new RuntimeException("Failed to URL encode ["
                            + this.compactEntityReferenceSerializer.serialize(reference) + "] using UTF-8.", e);
                }
            }
        }

        DocumentReference documentReference = resolveDocumentReference(documentResourceReference);
        return this.documentAccessBridge.getDocumentURL(documentReference, "create", modifiedQueryString,
                documentResourceReference.getParameter(DocumentResourceReference.ANCHOR));
    }

    /**
     * Resolve the reference passed as a String into a DocumentReference object.
     *
     * @param documentResourceReference the resource to transform into a {@link DocumentReference} object
     * @return the resolved reference resolved using the base resource reference if any
     */
    private DocumentReference resolveDocumentReference(ResourceReference documentResourceReference) {
        DocumentReference documentReference;
        if (!documentResourceReference.getBaseReferences().isEmpty()) {
            // If the passed reference has a base reference, resolve it first with a current resolver (it should
            // normally be absolute but who knows what the API caller has specified...)
            DocumentReference baseReference = resolveBaseReference(documentResourceReference.getBaseReferences());
            documentReference = this.currentDocumentReferenceResolver
                    .resolve(documentResourceReference.getReference(), baseReference);
        } else {
            documentReference = this.currentDocumentReferenceResolver
                    .resolve(documentResourceReference.getReference());
        }

        return documentReference;
    }

    /**
     * Resolve the list of base references taking the first element in the list, resolving it and then resolving the
     * other elements based on the previous resolving.
     *
     * @param baseReferences the list of base references to resolve
     * @return the resolved base reference against which to resolve the target resource reference
     */
    private DocumentReference resolveBaseReference(List<String> baseReferences) {
        DocumentReference resolvedBaseReference = null;
        for (String baseReference : baseReferences) {
            if (resolvedBaseReference != null) {
                resolvedBaseReference = this.currentDocumentReferenceResolver.resolve(baseReference,
                        resolvedBaseReference);
            } else {
                resolvedBaseReference = this.currentDocumentReferenceResolver.resolve(baseReference);
            }
        }
        return resolvedBaseReference;
    }

    /**
     * Resolve the reference passed as a String into an AttachmentReference object.
     *
     * @param attachmentResourceReference the resource to transform into a {@link AttachmentReference} object
     * @return the resolved reference resolved using the base resource reference if any
     */
    private AttachmentReference resolveAttachmentReference(ResourceReference attachmentResourceReference) {
        AttachmentReference attachmentReference;
        if (!attachmentResourceReference.getBaseReferences().isEmpty()) {
            // If the passed reference has a base reference, resolve it first with a current resolver (it should
            // normally be absolute but who knows what the API caller has specified...)
            DocumentReference baseReference = resolveBaseReference(attachmentResourceReference.getBaseReferences());
            attachmentReference = this.currentAttachmentReferenceResolver
                    .resolve(attachmentResourceReference.getReference(), baseReference);
        } else {
            attachmentReference = this.currentAttachmentReferenceResolver
                    .resolve(attachmentResourceReference.getReference());
        }

        return attachmentReference;
    }

    /**
     * Extracts the specified image dimension from the image parameters.
     *
     * @param dimension either {@code width} or {@code height}
     * @param imageParameters the image parameters; may include the {@code width}, {@code height} and {@code style}
     *            parameters
     * @return the value of the passed dimension if it is specified in the image parameters, {@code null} otherwise
     */
    private String getImageDimension(String dimension, Map<String, String> imageParameters) {
        // Check first if the style parameter contains information about the given dimension. In-line style has priority
        // over the dimension parameters.
        String value = null;
        String style = imageParameters.get("style");
        if (StringUtils.isNotBlank(style)) {
            try {
                CSSStyleDeclaration sd = this.cssParser
                        .parseStyleDeclaration(new InputSource(new StringReader(style)));
                value = sd.getPropertyValue(dimension);
            } catch (IOException e) {
                // Ignore the style parameter.
            }
        }
        if (StringUtils.isBlank(value)) {
            // Fall back on the value of the dimension parameter.
            value = imageParameters.get(dimension);
        }
        return value;
    }

    /**
     * Creates the query string that can be added to an image URL to resize the image on the server side.
     *
     * @param imageParameters image parameters, including width and height then they are specified
     * @return the query string to be added to an image URL in order to resize the image on the server side
     */
    private StringBuilder getImageURLQueryString(Map<String, String> imageParameters) {
        String width = StringUtils.chomp(getImageDimension(WIDTH, imageParameters), PIXELS);
        String height = StringUtils.chomp(getImageDimension(HEIGHT, imageParameters), PIXELS);
        boolean useHeight = StringUtils.isNotEmpty(height) && StringUtils.isNumeric(height);
        StringBuilder queryString = new StringBuilder();
        if (StringUtils.isEmpty(width) || !StringUtils.isNumeric(width)) {
            // Width is unspecified or is not measured in pixels.
            if (useHeight) {
                // Height is specified in pixels.
                queryString.append('&').append(HEIGHT).append('=').append(height);
            } else {
                // If image width and height are unspecified or if they are not expressed in pixels then limit the image
                // size to best fit the rectangle specified in the configuration (keeping aspect ratio).
                int widthLimit = this.xwikiRenderingConfiguration.getImageWidthLimit();
                if (widthLimit > 0) {
                    queryString.append('&').append(WIDTH).append('=').append(widthLimit);
                }
                int heightLimit = this.xwikiRenderingConfiguration.getImageHeightLimit();
                if (heightLimit > 0) {
                    queryString.append('&').append(HEIGHT).append('=').append(heightLimit);
                }
                if (widthLimit > 0 && heightLimit > 0) {
                    queryString.append("&keepAspectRatio=").append(true);
                }
            }
        } else {
            // Width is specified in pixels.
            queryString.append('&').append(WIDTH).append('=').append(width);
            if (useHeight) {
                // Height is specified in pixels.
                queryString.append('&').append(HEIGHT).append('=').append(height);
            }
        }
        return queryString;
    }
}