Commit acd367bd authored by Oscar Scholten's avatar Oscar Scholten

ESSENTIALS-999 removing components/cms from this repository

parent b8a80300
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2014-2015 Hippo B.V. (http://www.onehippo.com)
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.onehippo.cms7</groupId>
<artifactId>hippo-essentials-components</artifactId>
<version>3.1.0-SNAPSHOT</version>
</parent>
<name>Hippo Essentials CMS Component Library</name>
<description>Hippo Essentials CMS Component Library</description>
<artifactId>hippo-essentials-components-cms</artifactId>
<properties>
<jsoup.version>1.7.1</jsoup.version>
<rome.version>0.9</rome.version>
</properties>
<dependencies>
<dependency>
<!-- jsoup HTML parser library @ http://jsoup.org/ -->
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>${jsoup.version}</version>
</dependency>
<dependency>
<groupId>org.onehippo.cms7</groupId>
<artifactId>hippo-repository-connector</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>rome</groupId>
<artifactId>rome</artifactId>
<version>${rome.version}</version>
</dependency>
</dependencies>
<build>
<defaultGoal>package</defaultGoal>
</build>
</project>
/*
* Copyright 2015 Hippo B.V. (http://www.onehippo.com)
*
* 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.
*/
package org.onehippo.cms7.essentials.components.cms.blog;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import org.apache.commons.lang.ArrayUtils;
import org.hippoecm.repository.util.JcrUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class BlogImporterConfiguration {
public static final Logger log = LoggerFactory.getLogger(BlogImporterConfiguration.class);
private static final String PROP_CRONEXPRESSION = "cronExpression";
private static final String PROP_ACTIVE = "active";
private static final String PROP_RUNNOW = "runInstantly";
private final String cronExpression;
private final Boolean active;
private final Boolean runNow;
private final String blogBasePath;
private final String authorsBasePath;
private final String projectNamespace;
private final List<String> urls;
private final List<String> authors;
public BlogImporterConfiguration(Node moduleConfigNode) throws RepositoryException {
cronExpression = JcrUtils.getStringProperty(moduleConfigNode, PROP_CRONEXPRESSION, null);
active = JcrUtils.getBooleanProperty(moduleConfigNode, PROP_ACTIVE, Boolean.FALSE);
runNow = JcrUtils.getBooleanProperty(moduleConfigNode, PROP_RUNNOW, Boolean.FALSE);
projectNamespace = JcrUtils.getStringProperty(moduleConfigNode, BlogImporterJob.PROJECT_NAMESPACE, null);
blogBasePath = JcrUtils.getStringProperty(moduleConfigNode, BlogImporterJob.BLOGS_BASE_PATH, null);
authorsBasePath = JcrUtils.getStringProperty(moduleConfigNode, BlogImporterJob.AUTHORS_BASE_PATH, null);
urls = Arrays.asList(JcrUtils.getMultipleStringProperty(moduleConfigNode, BlogImporterJob.URLS, ArrayUtils.EMPTY_STRING_ARRAY));
authors = Arrays.asList(JcrUtils.getMultipleStringProperty(moduleConfigNode, BlogImporterJob.AUTHORS, ArrayUtils.EMPTY_STRING_ARRAY));
if (authors.size() != urls.size()) {
log.error("Authors and URL size mismatch, no blogs will be imported.");
authors.clear();
urls.clear();
}
}
public String getCronExpression() {
return cronExpression;
}
public boolean isActive() {
return active;
}
public boolean isRunNow() {
return runNow;
}
public String getBlogBasePath() {
return blogBasePath;
}
public String getAuthorsBasePath() {
return authorsBasePath;
}
public String getProjectNamespace() {
return projectNamespace;
}
public List<String> getUrls() {
return Collections.unmodifiableList(urls);
}
public List<String> getAuthors() {
return Collections.unmodifiableList(authors);
}
}
/*
* Copyright 2014 Hippo B.V. (http://www.onehippo.com)
*
* 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.
*/
package org.onehippo.cms7.essentials.components.cms.blog;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import javax.jcr.NamespaceException;
import javax.jcr.NamespaceRegistry;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.Property;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import org.onehippo.cms7.essentials.components.cms.handlers.HandlerUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
/**
* The BlogUpdater derives the bloggers' names from the linked Author document and stores them in the blog's
* AuthorNames meta data field. That field is/will be used for faceted navigation.
*/
public final class BlogUpdater {
private static final Logger log = LoggerFactory.getLogger(BlogUpdater.class);
private BlogUpdater() {
}
/**
* Indicate if the document variant represented by node should be handled.
*
* @param node JCR node to consider
* @return true if the node is interesting, false otherwise.
* @throws javax.jcr.RepositoryException
*/
public static boolean wants(final Node node, final String documentType) throws RepositoryException {
// check if namespace is registered namespace, skip otherwise:
if(Strings.isNullOrEmpty(documentType) || documentType.indexOf(':')==-1){
return false;
}
final Iterable<String> iterable = Splitter.on(':').omitEmptyStrings().trimResults().split(documentType);
final Iterator<String> iterator = iterable.iterator();
if(iterator.hasNext()){
final String namespacePrefix = iterator.next();
if(!namespacePrefixExists(node.getSession(), namespacePrefix)){
return false;
}
}
return node.getPrimaryNodeType().isNodeType(documentType);
}
public static boolean namespacePrefixExists(final Session session, final String prefix) {
try {
final NamespaceRegistry namespaceRegistry = session.getWorkspace().getNamespaceRegistry();
// Check whether a URI is mapped for the prefix
final String p = namespaceRegistry.getURI(prefix);
return !Strings.isNullOrEmpty(p);
} catch (NamespaceException e) {
// expected:
return false;
} catch (RepositoryException e) {
log.error("Error while determining namespace check.", e);
}
return false;
}
/**
* Handle the Save event for a blogpost document.
*
* @param blogpost JCR node representing the blogpost
* @return indication whether or not changes need to be saved.
* @throws javax.jcr.RepositoryException
*/
public static boolean handleSaved(final Node blogpost, final String projectNamespace) throws RepositoryException {
// Delete the old property
final String authorNamesProp = projectNamespace + ":authornames";
if (blogpost.hasProperty(authorNamesProp)) {
blogpost.getProperty(authorNamesProp).remove();
}
// Construct the new property
final NodeIterator authorMirrors = blogpost.getNodes(projectNamespace + ":authors");
final Collection<String> authorNames = new ArrayList<String>();
// TODO mm check this (was :title???)
final String authorProperty = projectNamespace + ":fullname";
while (authorMirrors.hasNext()) {
final Node mirror = authorMirrors.nextNode();
final Node author = HandlerUtils.getReferencedVariant(mirror, "published");
if (author != null) {
if (author.hasProperty(authorProperty)) {
final Property property = author.getProperty(authorProperty);
authorNames.add(property.getString());
} else {
log.warn("Author node:[{}] has no property:[{}]", author.getPath(), authorProperty);
}
} else {
log.warn("Author property couldn't be updated because referenced author node is not published yet: {}", mirror.getPath());
}
}
if (authorNames.size() > 0) {
blogpost.setProperty(authorNamesProp, authorNames.toArray(new String[authorNames.size()]));
}
return true;
}
}
/*
* Copyright 2014-2015 Hippo B.V. (http://www.onehippo.com)
*
* 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.
*/
package org.onehippo.cms7.essentials.components.cms.handlers;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import org.onehippo.cms7.essentials.components.cms.blog.BlogUpdater;
import org.onehippo.cms7.event.HippoEvent;
import org.onehippo.cms7.event.HippoEventConstants;
import org.onehippo.cms7.services.eventbus.Subscribe;
import org.onehippo.repository.events.HippoWorkflowEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Strings;
/**
* @version "$Id$"
*/
public class AuthorFieldHandler implements WorkflowEventHandler {
private static Logger log = LoggerFactory.getLogger(AuthorFieldHandler.class);
// Regrettably, the CMS doesn't expose below string in an API...
private static final String METHOD_NAME_SAVE = "commitEditableInstance";
private final Session session;
private final String projectNamespacePrefix;
public AuthorFieldHandler(final String projectNamespacePrefix, final Session session) {
this.projectNamespacePrefix = projectNamespacePrefix;
this.session = session;
}
/**
* Dispatch the event per workflow action.
*
* @param event the event.
*/
@Override
@Subscribe
public void handle(final HippoEvent<?> event) {
if (Strings.isNullOrEmpty(projectNamespacePrefix)) {
return;
}
if (HippoEventConstants.CATEGORY_WORKFLOW.equals(event.category())) {
HippoWorkflowEvent<?> wfEvent = new HippoWorkflowEvent(event);
if (METHOD_NAME_SAVE.equals(wfEvent.action())) {
dispatchSaveEvent(wfEvent, session);
}
}
}
/**
* Dispatch a "save" event.
* Derive the unpublished variant of the document the save event pertains to, and check who wants it.
*
* @param event the event.
* @param session the JCR session
*/
@SuppressWarnings("HippoHstCallNodeRefreshInspection")
private void dispatchSaveEvent(HippoWorkflowEvent<?> event, final Session session) {
final String handleUuid = event.subjectId();
try {
final Node handle = session.getNodeByIdentifier(handleUuid);
final Node variant = HandlerUtils.getVariant(handle, "unpublished");
if (variant != null && BlogUpdater.wants(variant, projectNamespacePrefix + ":blogpost")) {
if (BlogUpdater.handleSaved(variant, projectNamespacePrefix)) {
session.save();
}
}
} catch (RepositoryException ex) {
log.debug("Failed to process node for handle UUID '" + handleUuid + "'.", ex);
try {
session.refresh(false);
} catch (RepositoryException e) {
log.error("Error refreshing session", e);
}
}
}
}
/*
* Copyright 2014 Hippo B.V. (http://www.onehippo.com)
*
* 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.
*/
package org.onehippo.cms7.essentials.components.cms.handlers;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
/**
* @version "$Id$"
*/
public final class HandlerUtils {
private HandlerUtils() {
}
/**
* @param handle JCR node representing a handle
* @param state desired state of the variant
* @return JCR node representing that variant, or null.
* @throws javax.jcr.RepositoryException
*/
public static Node getVariant(final Node handle, final String state) throws RepositoryException {
final NodeIterator variants = handle.getNodes(handle.getName());
while (variants.hasNext()) {
final Node variant = variants.nextNode();
if (variant.hasProperty("hippostd:state") && variant.getProperty("hippostd:state").getString().equals(state)) {
return variant;
}
}
return null;
}
/**
* Helper function to derive a certain document variant, given a hippo:mirror node.
* Optimally, the CMS or repository would provide this functionality.
*
* @param mirror repository node of type hippo:mirror
* @param state desired state of the variant
* @return JCR node representing that variant, or null.
* @throws javax.jcr.RepositoryException
*/
public static Node getReferencedVariant(final Node mirror, final String state) throws RepositoryException {
final Session session = mirror.getSession();
final String rootUuid = session.getRootNode().getIdentifier();
final String uuid = mirror.getProperty("hippo:docbase").getString();
Node variant = null;
if (!rootUuid.equals(uuid)) {
final Node authorHandle = session.getNodeByIdentifier(uuid);
variant = getVariant(authorHandle, state);
}
return variant;
}
}
/*
* Copyright 2014 Hippo B.V. (http://www.onehippo.com)
*
* 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.
*/
package org.onehippo.cms7.essentials.components.cms.handlers;
import org.onehippo.cms7.event.HippoEvent;
/**
* @version "$Id$"
*/
public interface WorkflowEventHandler {
void handle(HippoEvent<?> event);
}
/*
* Copyright 2014-2015 Hippo B.V. (http://www.onehippo.com)
*
* 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.
*/
package org.onehippo.cms7.essentials.components.cms.modules;
import javax.jcr.*;
import javax.jcr.observation.Event;
import com.google.common.base.Joiner;
import org.apache.jackrabbit.api.observation.JackrabbitEvent;
import org.onehippo.cms7.essentials.components.cms.blog.BlogImporterConfiguration;
import org.onehippo.cms7.essentials.components.cms.blog.BlogImporterJob;
import org.onehippo.cms7.essentials.components.cms.handlers.AuthorFieldHandler;
import org.onehippo.cms7.services.HippoServiceRegistry;
import org.onehippo.cms7.services.eventbus.HippoEventBus;
import org.onehippo.repository.modules.AbstractReconfigurableDaemonModule;
import org.onehippo.repository.modules.RequiresService;
import org.onehippo.repository.scheduling.RepositoryJobCronTrigger;
import org.onehippo.repository.scheduling.RepositoryJobInfo;
import org.onehippo.repository.scheduling.RepositoryJobTrigger;
import org.onehippo.repository.scheduling.RepositoryScheduler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Strings;
/**
* The BlogListenerModule is a daemon module started by the CMS (based on the corresponding hippo:modules
* repository configuration). It has two main objectives:
*
* 1) register the AuthorFieldHandler with the HippoEventBus, such that the saving of blog post documents
* triggers the update of the blog post's author field.
* 2) schedule (and reschedule upon reconfiguration) the blog importer job.
*/
@RequiresService(types = {RepositoryScheduler.class})
public class BlogListenerModule extends AbstractReconfigurableDaemonModule {
public static final Logger log = LoggerFactory.getLogger(BlogListenerModule.class);
private static final String CONFIG_LOCK_ISDEEP_PROPERTY = "jcr:lockIsDeep";
private static final String CONFIG_LOCK_OWNER = "jcr:lockOwner";
private static final String SCHEDULER_NAME = "BlogListenerModule";
private static final String SCHEDULER_GROUP_NAME = "essentials";
private static final String JOB_NAME = SCHEDULER_NAME + "Job";
private AuthorFieldHandler listener;
@Override
protected void doConfigure(final Node moduleConfigNode) throws RepositoryException {
// we read the BlogImporterConfiguration on the fly.
}
@Override
protected void doInitialize(final Session session) throws RepositoryException {
final BlogImporterConfiguration config = new BlogImporterConfiguration(session.getNode(moduleConfigPath));
rescheduleJob(config);
registerListener(config);
}
@Override
protected void doShutdown() {
unregisterListener();
}
@Override
protected boolean isReconfigureEvent(Event event) throws RepositoryException {
String eventPath = event.getPath();
return !((JackrabbitEvent) event).isExternal()
&& !eventPath.endsWith(CONFIG_LOCK_ISDEEP_PROPERTY)
&& !eventPath.endsWith(CONFIG_LOCK_OWNER);
}
/**
* Rescheduling a job writes to the repository, which triggers a warning when done from the "notification thread".
* To avoid this warning, we run the job rescheduling as a separate thread.
*/
@Override
protected void onConfigurationChange(final Node moduleConfigNode) throws RepositoryException {
// Keep the reading of the configuration outside the thread, so we don't use
// the JCR session (by means of the moduleConfigNode) in a different thread.
final BlogImporterConfiguration config = new BlogImporterConfiguration(moduleConfigNode);
new Thread() {
@Override
public void run() {
rescheduleJob(config);
}
}.start();
}
private static synchronized void rescheduleJob(final BlogImporterConfiguration config) {
final RepositoryScheduler scheduler = HippoServiceRegistry.getService(RepositoryScheduler.class);
try {
if (scheduler.checkExists(JOB_NAME, SCHEDULER_GROUP_NAME)) {
scheduler.deleteJob(JOB_NAME, SCHEDULER_GROUP_NAME);
}
if (!config.isActive()
|| Strings.isNullOrEmpty(config.getCronExpression())
|| Strings.isNullOrEmpty(config.getProjectNamespace())
|| config.getUrls().isEmpty()) {
return; // No need to reschedule, we're done.
}
final RepositoryJobTrigger trigger = new RepositoryJobCronTrigger(SCHEDULER_NAME + "Trigger", config.getCronExpression());
final RepositoryJobInfo jobInfo = new RepositoryJobInfo(JOB_NAME, SCHEDULER_GROUP_NAME, BlogImporterJob.class);
populateJobInfo(jobInfo, config);
scheduler.scheduleJob(jobInfo, trigger);
if (config.isRunNow()) {
scheduler.executeJob(JOB_NAME, SCHEDULER_GROUP_NAME);
}
} catch (RepositoryException ex) {
log.error("Failure (re)scheduling the blog importer.", ex);
}
}
private static void populateJobInfo(final RepositoryJobInfo jobInfo, final BlogImporterConfiguration config) {
jobInfo.setAttribute(BlogImporterJob.PROJECT_NAMESPACE, config.getProjectNamespace());
jobInfo.setAttribute(BlogImporterJob.AUTHORS_BASE_PATH, config.getAuthorsBasePath());
jobInfo.setAttribute(BlogImporterJob.BLOGS_BASE_PATH, config.getBlogBasePath());
jobInfo.setAttribute(BlogImporterJob.AUTHORS, Joiner.on(BlogImporterJob.SPLITTER).join(config.getAuthors()));
jobInfo.setAttribute(BlogImporterJob.URLS, Joiner.on(BlogImporterJob.SPLITTER).join(config.getUrls()));
}
private void registerListener(final BlogImporterConfiguration config) {
if (!Strings.isNullOrEmpty(config.getProjectNamespace())) {
listener = new AuthorFieldHandler(config.getProjectNamespace(), session);
HippoServiceRegistry.registerService(listener, HippoEventBus.class);
} else {
log.warn("No projectNamespace configured in [org.onehippo.cms7.essentials.components.cms.modules.EventBusListenerModule]");
}
}
private void unregisterListener() {
if (listener != null) {
HippoServiceRegistry.unregisterService(listener, HippoEventBus.class);
}
}
}
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2014 Hippo B.V. (http://www.onehippo.com)
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