it.tidalwave.northernwind.frontend.ui.component.blog.DefaultBlogViewController.java Source code

Java tutorial

Introduction

Here is the source code for it.tidalwave.northernwind.frontend.ui.component.blog.DefaultBlogViewController.java

Source

/*
 * #%L
 * *********************************************************************************************************************
 *
 * NorthernWind - lightweight CMS
 * http://northernwind.tidalwave.it - git clone https://bitbucket.org/tidalwave/northernwind-src.git
 * %%
 * Copyright (C) 2011 - 2016 Tidalwave s.a.s. (http://tidalwave.it)
 * %%
 * *********************************************************************************************************************
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations under the License.
 *
 * *********************************************************************************************************************
 *
 * $Id$
 *
 * *********************************************************************************************************************
 * #L%
 */
package it.tidalwave.northernwind.frontend.ui.component.blog;

import javax.annotation.PostConstruct;
import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.time.ZoneId;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Configurable;
import it.tidalwave.util.Finder;
import it.tidalwave.util.Key;
import it.tidalwave.util.NotFoundException;
import it.tidalwave.util.spi.SimpleFinderSupport;
import it.tidalwave.northernwind.core.model.Content;
import it.tidalwave.northernwind.core.model.HttpStatusException;
import it.tidalwave.northernwind.core.model.RequestContext;
import it.tidalwave.northernwind.core.model.ResourcePath;
import it.tidalwave.northernwind.core.model.ResourceProperties;
import it.tidalwave.northernwind.core.model.Site;
import it.tidalwave.northernwind.core.model.SiteNode;
import it.tidalwave.northernwind.core.model.spi.RequestHolder;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import static it.tidalwave.northernwind.core.model.Content.Content;
import static it.tidalwave.northernwind.frontend.ui.component.Properties.*;
import static it.tidalwave.northernwind.frontend.ui.component.blog.BlogViewController.*;

/***********************************************************************************************************************
 *
 * @author  Fabrizio Giudici
 * @version $Id$
 *
 **********************************************************************************************************************/
@Configurable
@RequiredArgsConstructor
@Slf4j
public abstract class DefaultBlogViewController implements BlogViewController {
    @AllArgsConstructor
    @Getter
    @ToString
    protected static class TagAndCount {
        public final String tag;
        public int count;
        public String rank;
    }

    public static final List<Key<String>> DATE_KEYS = Arrays.asList(PROPERTY_PUBLISHING_DATE,
            PROPERTY_CREATION_DATE);

    public static final ZonedDateTime TIME0 = Instant.ofEpochMilli(0).atZone(ZoneId.of("GMT"));

    protected static final String TAG_PREFIX = "tag/";

    private final Comparator<Content> REVERSE_DATE_COMPARATOR = new Comparator<Content>() {
        @Override
        public int compare(final @Nonnull Content post1, final @Nonnull Content post2) {
            final ZonedDateTime dateTime1 = post1.getProperties().getDateTimeProperty(DATE_KEYS, TIME0);
            final ZonedDateTime dateTime2 = post2.getProperties().getDateTimeProperty(DATE_KEYS, TIME0);
            return dateTime2.compareTo(dateTime1);
        }
    };

    @Nonnull
    protected final BlogView view;

    @Nonnull
    private final SiteNode siteNode;

    @Nonnull
    private final Site site;

    @Nonnull
    private final RequestHolder requestHolder;

    @Nonnull
    protected final RequestContext requestContext;

    /*******************************************************************************************************************
     *
     *
     ******************************************************************************************************************/
    // FIXME: add eventual localized versions
    @Override
    @Nonnull
    public Finder<SiteNode> findChildrenSiteNodes() {
        return new SimpleFinderSupport<SiteNode>() {
            @Override
            protected List<? extends SiteNode> computeResults() {
                log.info("findCompositeContents()");
                final List<SiteNode> results = new ArrayList<>();

                try {
                    final ResourceProperties componentProperties = siteNode.getPropertyGroup(view.getId());

                    for (final Content post : findAllPosts(componentProperties)) {
                        try {
                            final ResourcePath relativeUri = siteNode.getRelativeUri()
                                    .appendedWith(post.getExposedUri());
                            results.add(new ChildSiteNode(siteNode, relativeUri, post.getProperties()));
                        } catch (NotFoundException | IOException e) {
                            log.warn("While reading properties", e);
                        }
                    }
                } catch (NotFoundException | IOException e) {
                    log.warn("While reading property group", e);
                }

                log.info(">>>> returning: {}", results);

                return results;
            }
        };
    }

    /*******************************************************************************************************************
     *
     * Initializes this controller.
     *
     ******************************************************************************************************************/
    @PostConstruct
    protected void initialize() throws Exception {
        // FIXME: ugly workaround for a design limitation. See NW-110.
        if (isCalledBySitemapController()) // called as a CompositeContentsController
        {
            return;
        }

        log.info("Initializing for {}", siteNode);
        // called at initialization
        try {
            final ResourceProperties siteNodeProperties = siteNode.getPropertyGroup(view.getId());
            final boolean tagCloud = siteNodeProperties.getBooleanProperty(PROPERTY_TAG_CLOUD, false);

            if (tagCloud) {
                generateTagCloud();
            } else {
                generateBlogPosts();
            }

            render();
        }
        // FIXME: this happens when somebody tries to render a blog folder, which shouldn't happen
        catch (NotFoundException | IOException e) {
            log.warn("While reading property group at initialization", e);
        }
    }

    /*******************************************************************************************************************
     *
     * Renders the blog posts.
     *
     ******************************************************************************************************************/
    private void generateBlogPosts() throws IOException, NotFoundException, HttpStatusException {
        final ResourceProperties componentProperties = siteNode.getPropertyGroup(view.getId());
        final int maxFullItems = componentProperties.getIntProperty(PROPERTY_MAX_FULL_ITEMS, 99);
        final int maxLeadinItems = componentProperties.getIntProperty(PROPERTY_MAX_LEADIN_ITEMS, 99);
        final int maxItems = componentProperties.getIntProperty(PROPERTY_MAX_ITEMS, 99);

        log.debug(">>>> rendering blog posts for {}: maxFullItems: {}, maxLeadinItems: {}, maxItems: {}",
                view.getId(), maxFullItems, maxLeadinItems, maxItems);

        int currentItem = 0;

        for (final Content post : findPostsInReverseDateOrder(componentProperties)) {
            try {
                log.debug(">>>>>>> processing blog item #{}: {}", currentItem, post);
                // FIXME: use hasProperty() and use PROPERTY_FULLTEXT
                post.getProperties().getProperty(PROPERTY_TITLE); // Skip folders used for categories - this throws exception

                if (currentItem < maxFullItems) {
                    addFullPost(post);
                } else if (currentItem < maxFullItems + maxLeadinItems) {
                    addLeadInPost(post);
                } else if (currentItem < maxItems) {
                    addReference(post);
                }

                currentItem++;
            } catch (NotFoundException e) {
                log.info("While reading property group of post {}", e.toString());
            } catch (IOException e) {
                log.warn("While reading property group of post", e);
            }
        }
    }

    /*******************************************************************************************************************
     *
     * Renders the blog posts.
     *
     ******************************************************************************************************************/
    private void generateTagCloud() throws IOException, NotFoundException, HttpStatusException {
        final Map<String, TagAndCount> tagAndCountMapByTag = new TreeMap<>();
        final ResourceProperties siteNodeProperties = siteNode.getPropertyGroup(view.getId());

        for (final Content post : findAllPosts(siteNodeProperties)) {
            try {
                for (final String tag : post.getProperties().getProperty(PROPERTY_TAGS).split(",")) {
                    TagAndCount tagAndCount = tagAndCountMapByTag.get(tag);

                    if (tagAndCount == null) {
                        tagAndCount = new TagAndCount(tag, 0, "");
                        tagAndCountMapByTag.put(tag, tagAndCount);
                    }

                    tagAndCount.count++;
                }
            } catch (NotFoundException | IOException e) {
                // ok, not tag
            }
        }

        final Collection<TagAndCount> tagsAndCount = tagAndCountMapByTag.values();
        computeRanks(tagsAndCount);
        addTagCloud(tagsAndCount);
    }

    /*******************************************************************************************************************
     *
     * Finds all the posts.
     *
     ******************************************************************************************************************/
    @Nonnull
    private List<Content> findAllPosts(final @Nonnull ResourceProperties siteNodeProperties)
            throws NotFoundException, IOException {
        final List<Content> allPosts = new ArrayList<>();

        for (final String relativePath : siteNodeProperties.getProperty(PROPERTY_CONTENTS)) {
            final Content postsFolder = site.find(Content).withRelativePath(relativePath).result();
            allPosts.addAll(postsFolder.findChildren().results());
        }

        log.debug(">>>> all posts: {}", allPosts.size());

        return allPosts;
    }

    /*******************************************************************************************************************
     *
     *
     ******************************************************************************************************************/
    // TODO: embed the sort by reverse date in the finder
    @Nonnull
    private List<Content> findPostsInReverseDateOrder(final @Nonnull ResourceProperties siteNodeProperties)
            throws IOException, NotFoundException, HttpStatusException {
        final String pathParams = requestHolder.get().getPathParams(siteNode).replaceFirst("^/", "");
        final boolean index = siteNodeProperties.getBooleanProperty(PROPERTY_INDEX, false);
        final List<Content> allPosts = findAllPosts(siteNodeProperties);
        final List<Content> posts = new ArrayList<>();
        //
        // The thing work differently in function of pathParams:
        // + when no pathParams, it returns all the posts
        // + when it matches a category, it returns all the posts in that category
        // + when it matches an exposed URI of a single specific post, and not in 'index' mode it returns only that
        //   post; if in 'index' mode, it returns all the posts.
        //
        if ("".equals(pathParams)) {
            posts.addAll(allPosts);
        } else {
            if (pathParams.startsWith(TAG_PREFIX)) {
                final String tag = pathParams.replaceFirst("^" + TAG_PREFIX, "");
                filterByTag(allPosts, posts, tag);
            } else {
                try {
                    final Content singlePost = findPostByExposedUri(allPosts, new ResourcePath(pathParams));
                    // pathParams matches an exposedUri; thus it's not a category, so an index wants all
                    posts.addAll(index ? allPosts : Collections.singletonList(singlePost));
                } catch (NotFoundException e) {
                    // pathParams didn't match an exposedUri, so it's interpreted as a category to filter posts
                    filterByCategory(allPosts, posts, pathParams);
                }
            }
        }

        // If not index mode, nothing found and searched for something in path params, return 404
        if (!index && !"".equals(pathParams) && posts.isEmpty()) {
            throw new HttpStatusException(404);
        }

        Collections.sort(posts, REVERSE_DATE_COMPARATOR);

        log.debug(">>>> found {} items", posts.size());

        return posts;
    }

    /*******************************************************************************************************************
     *
     * Adds to {@code destinationPosts} all the {@code sourcePosts} that matches the selected {@code category}; all
     * posts if the category is empty.
     *
     * @param  sourcePosts          the source posts
     * @param  destinationPosts     the destination posts
     * @param  category             the category
     *
     ******************************************************************************************************************/
    private void filterByCategory(final @Nonnull List<Content> sourcePosts,
            final @Nonnull List<Content> destinationPosts, final @Nonnull String category) {
        for (final Content post : sourcePosts) {
            try {
                if (category.equals("")
                        || category.equals(post.getProperties().getProperty(PROPERTY_CATEGORY, "---"))) {
                    destinationPosts.add(post);
                }
            } catch (IOException e2) {
                log.warn("", e2);
            }
        }
    }

    /*******************************************************************************************************************
     *
     * Adds to {@code destinationPosts} all the {@code sourcePosts} that matches the selected {@code tag}; all
     * posts if the category is empty.
     *
     * @param  sourcePosts          the source posts
     * @param  destinationPosts     the destination posts
     * @param  tag                  the tag
     *
     ******************************************************************************************************************/
    private void filterByTag(final @Nonnull List<Content> sourcePosts,
            final @Nonnull List<Content> destinationPosts, final @Nonnull String tag) {
        for (final Content post : sourcePosts) {
            try {
                final List<String> tags = Arrays
                        .asList(post.getProperties().getProperty(PROPERTY_TAGS, "").split(","));

                if (tags.contains(tag)) {
                    destinationPosts.add(post);
                }
            } catch (IOException e2) {
                log.warn("", e2);
            }
        }
    }

    /*******************************************************************************************************************
     *
     *
     ******************************************************************************************************************/
    @Nonnull
    private Content findPostByExposedUri(final List<Content> allPosts, final @Nonnull ResourcePath exposedUri)
            throws NotFoundException, IOException {
        for (final Content post : allPosts) {
            try {
                if (exposedUri.equals(post.getExposedUri())) {
                    return post;
                }
            } catch (NotFoundException e) {
                log.warn("{}", e.toString());
            } catch (IOException e) {
                log.warn("", e);
            }
        }

        throw new NotFoundException("Blog post with exposedUri=" + exposedUri.asString());
    }

    /*******************************************************************************************************************
     *
     *
     ******************************************************************************************************************/
    protected abstract void addFullPost(@Nonnull Content post) throws IOException, NotFoundException;

    /*******************************************************************************************************************
     *
     *
     ******************************************************************************************************************/
    protected abstract void addLeadInPost(@Nonnull Content post) throws IOException, NotFoundException;

    /*******************************************************************************************************************
     *
     *
     ******************************************************************************************************************/
    protected abstract void addReference(@Nonnull Content post) throws IOException, NotFoundException;

    /*******************************************************************************************************************
     *
     *
     ******************************************************************************************************************/
    protected abstract void addTagCloud(@Nonnull Collection<TagAndCount> tagsAndCount)
            throws IOException, NotFoundException;

    /*******************************************************************************************************************
     *
     *
     ******************************************************************************************************************/
    protected abstract void render() throws Exception;

    /*******************************************************************************************************************
     *
     *
     ******************************************************************************************************************/
    private void computeRanks(final @Nonnull Collection<TagAndCount> tagsAndCount) {
        final List<TagAndCount> tagsAndCountByCountDescending = new ArrayList<>(tagsAndCount);
        Collections.sort(tagsAndCountByCountDescending, new Comparator<TagAndCount>() {
            @Override
            public int compare(final @Nonnull TagAndCount tac1, final @Nonnull TagAndCount tac2) {
                return (int) Math.signum(tac2.count - tac1.count);
            }
        });

        int rank = 1;
        int previousCount = 0;

        for (final TagAndCount tac : tagsAndCountByCountDescending) {
            tac.rank = (rank <= 10) ? Integer.toString(rank) : "Others";

            if (previousCount != tac.count) {
                rank++;
            }

            previousCount = tac.count;
        }
    }

    /*******************************************************************************************************************
     *
     *
     ******************************************************************************************************************/
    private static boolean isCalledBySitemapController() {
        for (final StackTraceElement element : Thread.currentThread().getStackTrace()) {
            if (element.getClassName().contains("SitemapViewController")) {
                return true;
            }
        }

        return false;
    }
}