This is an automated email from the ASF dual-hosted git repository. rombert pushed a commit to annotated tag org.apache.sling.jcr.contentloader-2.0.2-incubator in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-jcr-contentloader.git
commit 769b49c65ca70e052893ec0369a07d20a0fd3029 Author: Felix Meschberger <[email protected]> AuthorDate: Thu Jun 18 09:21:02 2009 +0000 Move Sling to new TLP location git-svn-id: https://svn.eu.apache.org/repos/asf/sling/tags/org.apache.sling.jcr.contentloader-2.0.2-incubator@785979 13f79535-47bb-0310-9956-ffa450edef68 --- LICENSE | 202 ++++ NOTICE | 8 + README.txt | 38 + pom.xml | 132 +++ .../internal/ContentLoaderService.java | 340 +++++++ .../jcr/contentloader/internal/ImportProvider.java | 27 + .../jcr/contentloader/internal/JsonReader.java | 189 ++++ .../sling/jcr/contentloader/internal/Loader.java | 1040 ++++++++++++++++++++ .../contentloader/internal/NodeDescription.java | 153 +++ .../jcr/contentloader/internal/NodeReader.java | 31 + .../jcr/contentloader/internal/PathEntry.java | 140 +++ .../internal/PropertyDescription.java | 119 +++ .../jcr/contentloader/internal/XmlReader.java | 169 ++++ src/main/resources/META-INF/DISCLAIMER | 7 + src/main/resources/META-INF/LICENSE | 232 +++++ src/main/resources/META-INF/NOTICE | 11 + .../jcr/contentloader/internal/JsonReaderTest.java | 374 +++++++ 17 files changed, 3212 insertions(+) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..f31ecff --- /dev/null +++ b/NOTICE @@ -0,0 +1,8 @@ +Apache Sling Initial Content Loader +Copyright 2008 The Apache Software Foundation + +Apache Sling is based on source code originally developed +by Day Software (http://www.day.com/). + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..9c00331 --- /dev/null +++ b/README.txt @@ -0,0 +1,38 @@ +Apache Sling Initial Content Loader + +This bundle provides initial content installation through bundles. + + +Disclaimer +========== +Apache Sling is an effort undergoing incubation at The Apache Software Foundation (ASF), +sponsored by the Apache Jackrabbit PMC. Incubation is required of all newly accepted +projects until a further review indicates that the infrastructure, communications, +and decision making process have stabilized in a manner consistent with other +successful ASF projects. While incubation status is not necessarily a reflection of +the completeness or stability of the code, it does indicate that the project has yet +to be fully endorsed by the ASF. + +Getting Started +=============== + +This component uses a Maven 2 (http://maven.apache.org/) build +environment. It requires a Java 5 JDK (or higher) and Maven (http://maven.apache.org/) +2.0.7 or later. We recommend to use the latest Maven version. + +If you have Maven 2 installed, you can compile and +package the jar using the following command: + + mvn package + +See the Maven 2 documentation for other build features. + +The latest source code for this component is available in the +Subversion (http://subversion.tigris.org/) source repository of +the Apache Software Foundation. If you have Subversion installed, +you can checkout the latest source using the following command: + + svn checkout http://svn.apache.org/repos/asf/incubator/sling/trunk/jcr/contentloader + +See the Subversion documentation for other source control features. + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..55725f2 --- /dev/null +++ b/pom.xml @@ -0,0 +1,132 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> +<!-- + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you 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.apache.sling</groupId> + <artifactId>sling</artifactId> + <version>3-incubator</version> + <relativePath>../../parent/pom.xml</relativePath> + </parent> + + <artifactId>org.apache.sling.jcr.contentloader</artifactId> + <version>2.0.2-incubator</version> + <packaging>bundle</packaging> + + <name>Sling - Initial Content Loader</name> + <description> + This bundle provides initial content installation through bundles. + </description> + + <scm> + <connection>scm:svn:http://svn.apache.org/repos/asf/incubator/sling/tags/org.apache.sling.jcr.contentloader-2.0.2-incubator</connection> + <developerConnection>scm:svn:https://svn.apache.org/repos/asf/incubator/sling/tags/org.apache.sling.jcr.contentloader-2.0.2-incubator</developerConnection> + <url>http://svn.apache.org/viewvc/incubator/sling/tags/org.apache.sling.jcr.contentloader-2.0.2-incubator</url> + </scm> + + <build> + <plugins> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-scr-plugin</artifactId> + </plugin> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <extensions>true</extensions> + <configuration> + <instructions> + <Private-Package> + org.apache.sling.jcr.contentloader.internal.*, + org.kxml2.io, org.xmlpull.v1 + </Private-Package> + + <Embed-Dependency> + kxml2 + </Embed-Dependency> + + </instructions> + </configuration> + </plugin> + </plugins> + </build> + <reporting> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-javadoc-plugin</artifactId> + <configuration> + <excludePackageNames> + org.apache.sling.jcr.contentloader.internal + </excludePackageNames> + </configuration> + </plugin> + </plugins> + </reporting> + <dependencies> + <dependency> + <groupId>org.apache.felix</groupId> + <artifactId>org.osgi.core</artifactId> + </dependency> + <dependency> + <groupId>org.apache.felix</groupId> + <artifactId>org.osgi.compendium</artifactId> + </dependency> + <dependency> + <groupId>org.apache.sling</groupId> + <artifactId>org.apache.sling.jcr.api</artifactId> + <version>2.0.2-incubator</version> + </dependency> + <dependency> + <groupId>org.apache.sling</groupId> + <artifactId>org.apache.sling.commons.json</artifactId> + <version>2.0.2-incubator</version> + </dependency> + <dependency> + <groupId>org.apache.sling</groupId> + <artifactId>org.apache.sling.commons.mime</artifactId> + <version>2.0.2-incubator</version> + </dependency> + <dependency> + <groupId>org.apache.sling</groupId> + <artifactId>org.apache.sling.commons.osgi</artifactId> + <version>2.0.2-incubator</version> + </dependency> + <dependency> + <groupId>org.apache.sling</groupId> + <artifactId>org.apache.sling.engine</artifactId> + <version>2.0.2-incubator</version> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + </dependency> + <dependency> + <groupId>net.sf.kxml</groupId> + <artifactId>kxml2</artifactId> + <scope>provided</scope> + </dependency> + + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + </dependency> + </dependencies> +</project> diff --git a/src/main/java/org/apache/sling/jcr/contentloader/internal/ContentLoaderService.java b/src/main/java/org/apache/sling/jcr/contentloader/internal/ContentLoaderService.java new file mode 100644 index 0000000..347ce30 --- /dev/null +++ b/src/main/java/org/apache/sling/jcr/contentloader/internal/ContentLoaderService.java @@ -0,0 +1,340 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.sling.jcr.contentloader.internal; + +import java.util.Calendar; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.StringTokenizer; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.lock.LockException; + +import org.apache.sling.commons.mime.MimeTypeService; +import org.apache.sling.engine.SlingSettingsService; +import org.apache.sling.jcr.api.SlingRepository; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleEvent; +import org.osgi.framework.SynchronousBundleListener; +import org.osgi.service.component.ComponentContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The <code>ContentLoaderService</code> is the service + * providing the following functionality: + * <ul> + * <li>Bundle listener to load initial content. + * <li>Fires OSGi EventAdmin events on behalf of internal helper objects + * </ul> + * + * @scr.component metatype="no" + * @scr.property name="service.description" value="Sling + * Content Loader Implementation" + * @scr.property name="service.vendor" value="The Apache Software Foundation" + */ +public class ContentLoaderService implements SynchronousBundleListener { + + public static final String PROPERTY_CONTENT_LOADED = "content-loaded"; + + public static final String BUNDLE_CONTENT_NODE = "/var/sling/bundle-content"; + + /** default log */ + private final Logger log = LoggerFactory.getLogger(getClass()); + + /** + * The JCR Repository we access to resolve resources + * + * @scr.reference + */ + private SlingRepository repository; + + /** + * The MimeTypeService used by the initial content initialContentLoader to + * resolve MIME types for files to be installed. + * + * @scr.reference + */ + private MimeTypeService mimeTypeService; + + /** + * Administrative sessions used to check item existence. + */ + private Session adminSession; + + /** + * The initial content loader which is called to load initial content up + * into the repository when the providing bundle is installed. + */ + private Loader initialContentLoader; + + /** + * The id of the current instance + */ + private String slingId; + + /** + * List of currently updated bundles. + */ + private final Set<String> updatedBundles = new HashSet<String>(); + + /** @scr.reference + * Sling settings service. */ + protected SlingSettingsService settingsService; + + // ---------- BundleListener ----------------------------------------------- + + /** + * Loads and unloads any content provided by the bundle whose state + * changed. If the bundle has been started, the content is loaded. If + * the bundle is about to stop, the content are unloaded. + * + * @param event The <code>BundleEvent</code> representing the bundle state + * change. + */ + public void bundleChanged(BundleEvent event) { + + // + // NOTE: + // This is synchronous - take care to not block the system !! + // + + switch (event.getType()) { + case BundleEvent.STARTING: + // register content when the bundle content is available + // as node types are registered when the bundle is installed + // we can safely add the content at this point. + try { + Session session = getAdminSession(); + final boolean isUpdate = this.updatedBundles.remove(event.getBundle().getSymbolicName()); + initialContentLoader.registerBundle(session, event.getBundle(), isUpdate); + } catch (Throwable t) { + log.error( + "bundleChanged: Problem loading initial content of bundle " + + event.getBundle().getSymbolicName() + " (" + + event.getBundle().getBundleId() + ")", t); + } + break; + case BundleEvent.UPDATED: + // we just add the symbolic name to the list of updated bundles + // we will use this info when the new start event is triggered + this.updatedBundles.add(event.getBundle().getSymbolicName()); + break; + case BundleEvent.STOPPED: + try { + Session session = getAdminSession(); + initialContentLoader.unregisterBundle(session, event.getBundle()); + } catch (Throwable t) { + log.error( + "bundleChanged: Problem unloading initial content of bundle " + + event.getBundle().getSymbolicName() + " (" + + event.getBundle().getBundleId() + ")", t); + } + break; + } + } + + // ---------- Implementation helpers -------------------------------------- + + /** Returns the MIME type from the MimeTypeService for the given name */ + public String getMimeType(String name) { + // local copy to not get NPE despite check for null due to concurrent + // unbind + MimeTypeService mts = mimeTypeService; + return (mts != null) ? mts.getMimeType(name) : null; + } + + protected void createRepositoryPath(final Session writerSession, final String repositoryPath) + throws RepositoryException { + if ( !writerSession.itemExists(repositoryPath) ) { + Node node = writerSession.getRootNode(); + String path = repositoryPath.substring(1); + int pos = path.lastIndexOf('/'); + if ( pos != -1 ) { + final StringTokenizer st = new StringTokenizer(path.substring(0, pos), "/"); + while ( st.hasMoreTokens() ) { + final String token = st.nextToken(); + if ( !node.hasNode(token) ) { + node.addNode(token, "sling:Folder"); + node.save(); + } + node = node.getNode(token); + } + path = path.substring(pos + 1); + } + if ( !node.hasNode(path) ) { + node.addNode(path, "sling:Folder"); + node.save(); + } + } + } + + // ---------- SCR Integration --------------------------------------------- + + /** Activates this component, called by SCR before registering as a service */ + protected void activate(ComponentContext componentContext) { + this.slingId = this.settingsService.getSlingId(); + this.initialContentLoader = new Loader(this); + + componentContext.getBundleContext().addBundleListener(this); + + try { + final Session session = getAdminSession(); + this.createRepositoryPath(session, ContentLoaderService.BUNDLE_CONTENT_NODE); + log.debug( + "Activated - attempting to load content from all " + + "bundles which are neither INSTALLED nor UNINSTALLED"); + + int ignored = 0; + Bundle[] bundles = componentContext.getBundleContext().getBundles(); + for (Bundle bundle : bundles) { + if ((bundle.getState() & (Bundle.INSTALLED | Bundle.UNINSTALLED)) == 0) { + // load content for bundles which are neither INSTALLED nor + // UNINSTALLED + initialContentLoader.registerBundle(session, bundle, false); + } else { + ignored++; + } + + } + + log.debug( + "Out of {} bundles, {} were not in a suitable state for initial content loading", + bundles.length, ignored + ); + + } catch (Throwable t) { + log.error("activate: Problem while loading initial content and" + + " registering mappings for existing bundles", t); + } + } + + /** Deativates this component, called by SCR to take out of service */ + protected void deactivate(ComponentContext componentContext) { + componentContext.getBundleContext().removeBundleListener(this); + + if ( this.initialContentLoader != null ) { + this.initialContentLoader.dispose(); + this.initialContentLoader = null; + } + + if ( adminSession != null ) { + this.adminSession.logout(); + this.adminSession = null; + } + } + + // ---------- internal helper ---------------------------------------------- + + /** Returns the JCR repository used by this service. */ + protected SlingRepository getRepository() { + return repository; + } + + /** + * Returns an administrative session to the default workspace. + */ + private synchronized Session getAdminSession() + throws RepositoryException { + if ( adminSession == null ) { + adminSession = getRepository().loginAdministrative(null); + } + return adminSession; + } + + /** + * Return the bundle content info and make an exclusive lock. + * @param session + * @param bundle + * @return The map of bundle content info or null. + * @throws RepositoryException + */ + public Map<String, Object> getBundleContentInfo(final Session session, final Bundle bundle) + throws RepositoryException { + final String nodeName = bundle.getSymbolicName(); + final Node parentNode = (Node)session.getItem(BUNDLE_CONTENT_NODE); + if ( !parentNode.hasNode(nodeName) ) { + try { + final Node bcNode = parentNode.addNode(nodeName, "nt:unstructured"); + bcNode.addMixin("mix:lockable"); + parentNode.save(); + } catch (RepositoryException re) { + // for concurrency issues (running in a cluster) we ignore exceptions + this.log.warn("Unable to create node " + nodeName, re); + session.refresh(true); + } + } + final Node bcNode = parentNode.getNode(nodeName); + if ( bcNode.isLocked() ) { + return null; + } + try { + bcNode.lock(false, true); + } catch (LockException le) { + return null; + } + final Map<String, Object> info = new HashMap<String, Object>(); + if ( bcNode.hasProperty(ContentLoaderService.PROPERTY_CONTENT_LOADED) ) { + info.put(ContentLoaderService.PROPERTY_CONTENT_LOADED, + bcNode.getProperty(ContentLoaderService.PROPERTY_CONTENT_LOADED).getBoolean()); + } else { + info.put(ContentLoaderService.PROPERTY_CONTENT_LOADED, false); + } + return info; + } + + public void unlockBundleContentInfo(final Session session, + final Bundle bundle, + final boolean contentLoaded) + throws RepositoryException { + final String nodeName = bundle.getSymbolicName(); + final Node parentNode = (Node)session.getItem(BUNDLE_CONTENT_NODE); + final Node bcNode = parentNode.getNode(nodeName); + if ( contentLoaded ) { + bcNode.setProperty(ContentLoaderService.PROPERTY_CONTENT_LOADED, contentLoaded); + bcNode.setProperty("content-load-time", Calendar.getInstance()); + bcNode.setProperty("content-loaded-by", this.slingId); + bcNode.setProperty("content-unload-time", (String)null); + bcNode.setProperty("content-unloaded-by", (String)null); + bcNode.save(); + } + bcNode.unlock(); + } + + public void contentIsUninstalled(final Session session, + final Bundle bundle) { + final String nodeName = bundle.getSymbolicName(); + try { + final Node parentNode = (Node)session.getItem(BUNDLE_CONTENT_NODE); + if ( parentNode.hasNode(nodeName) ) { + final Node bcNode = parentNode.getNode(nodeName); + bcNode.setProperty(ContentLoaderService.PROPERTY_CONTENT_LOADED, false); + bcNode.setProperty("content-unload-time", Calendar.getInstance()); + bcNode.setProperty("content-unloaded-by", this.slingId); + bcNode.save(); + } + } catch (RepositoryException re) { + this.log.error("Unable to update bundle content info.", re); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/apache/sling/jcr/contentloader/internal/ImportProvider.java b/src/main/java/org/apache/sling/jcr/contentloader/internal/ImportProvider.java new file mode 100644 index 0000000..e28fd55 --- /dev/null +++ b/src/main/java/org/apache/sling/jcr/contentloader/internal/ImportProvider.java @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.sling.jcr.contentloader.internal; + +import java.io.IOException; + +interface ImportProvider { + + NodeReader getReader() throws IOException; + +} diff --git a/src/main/java/org/apache/sling/jcr/contentloader/internal/JsonReader.java b/src/main/java/org/apache/sling/jcr/contentloader/internal/JsonReader.java new file mode 100644 index 0000000..ef5c4b4 --- /dev/null +++ b/src/main/java/org/apache/sling/jcr/contentloader/internal/JsonReader.java @@ -0,0 +1,189 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.sling.jcr.contentloader.internal; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashSet; +import java.util.Set; + +import javax.jcr.PropertyType; + +import org.apache.sling.commons.json.JSONArray; +import org.apache.sling.commons.json.JSONException; +import org.apache.sling.commons.json.JSONObject; + + +/** + * The <code>JsonReader</code> TODO + */ +class JsonReader implements NodeReader { + + private static final Set<String> ignoredNames = new HashSet<String>(); + static { + ignoredNames.add("jcr:primaryType"); + ignoredNames.add("jcr:mixinTypes"); + ignoredNames.add("jcr:uuid"); + ignoredNames.add("jcr:baseVersion"); + ignoredNames.add("jcr:predecessors"); + ignoredNames.add("jcr:successors"); + ignoredNames.add("jcr:checkedOut"); + ignoredNames.add("jcr:created"); + } + + static final ImportProvider PROVIDER = new ImportProvider() { + private JsonReader jsonReader; + + public NodeReader getReader() { + if (jsonReader == null) { + jsonReader = new JsonReader(); + } + return jsonReader; + } + }; + + public NodeDescription parse(InputStream ins) throws IOException { + try { + String jsonString = toString(ins).trim(); + if (!jsonString.startsWith("{")) { + jsonString = "{" + jsonString + "}"; + } + + JSONObject json = new JSONObject(jsonString); + return this.createNode(null, json); + + } catch (JSONException je) { + throw (IOException) new IOException(je.getMessage()).initCause(je); + } + } + + protected NodeDescription createNode(String name, JSONObject obj) throws JSONException { + NodeDescription node = new NodeDescription(); + node.setName(name); + + Object primaryType = obj.opt("jcr:primaryType"); + if (primaryType != null) { + node.setPrimaryNodeType(String.valueOf(primaryType)); + } + + Object mixinsObject = obj.opt("jcr:mixinTypes"); + if (mixinsObject instanceof JSONArray) { + JSONArray mixins = (JSONArray) mixinsObject; + for (int i = 0; i < mixins.length(); i++) { + node.addMixinNodeType(mixins.getString(i)); + } + } + + // add properties and nodes + JSONArray names = obj.names(); + for (int i = 0; names != null && i < names.length(); i++) { + String n = names.getString(i); + // skip well known objects + if (!ignoredNames.contains(n)) { + Object o = obj.get(n); + if (o instanceof JSONObject) { + NodeDescription child = this.createNode(n, (JSONObject) o); + node.addChild(child); + } else if (o instanceof JSONArray) { + PropertyDescription prop = createProperty(n, o); + node.addProperty(prop); + } else { + PropertyDescription prop = createProperty(n, o); + node.addProperty(prop); + } + } + } + return node; + } + + protected PropertyDescription createProperty(String name, Object value) + throws JSONException { + PropertyDescription property = new PropertyDescription(); + property.setName(name); + + // assume simple value + if (value instanceof JSONArray) { + // multivalue + JSONArray array = (JSONArray) value; + if (array.length() > 0) { + for (int i = 0; i < array.length(); i++) { + property.addValue(array.get(i)); + } + value = array.opt(0); + } else { + property.addValue(null); + value = null; + } + + } else { + // single value + property.setValue(String.valueOf(value)); + } + // set type + property.setType(getType(value)); + + return property; + } + + protected String getType(Object object) { + if (object instanceof Double || object instanceof Float) { + return PropertyType.TYPENAME_DOUBLE; + } else if (object instanceof Number) { + return PropertyType.TYPENAME_LONG; + } else if (object instanceof Boolean) { + return PropertyType.TYPENAME_BOOLEAN; + } + + // fall back to default + return PropertyType.TYPENAME_STRING; + } + + private String toString(InputStream ins) throws IOException { + if (!ins.markSupported()) { + ins = new BufferedInputStream(ins); + } + + String encoding; + ins.mark(5); + int c = ins.read(); + if (c == '#') { + // character encoding following + StringBuffer buf = new StringBuffer(); + for (c = ins.read(); !Character.isWhitespace((char) c); c = ins.read()) { + buf.append((char) c); + } + encoding = buf.toString(); + } else { + ins.reset(); + encoding = "UTF-8"; + } + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + byte[] buf = new byte[1024]; + int rd; + while ( (rd = ins.read(buf)) >= 0) { + bos.write(buf, 0, rd); + } + bos.close(); // just to comply with the contract + + return new String(bos.toByteArray(), encoding); + } +} diff --git a/src/main/java/org/apache/sling/jcr/contentloader/internal/Loader.java b/src/main/java/org/apache/sling/jcr/contentloader/internal/Loader.java new file mode 100644 index 0000000..ca6c28f --- /dev/null +++ b/src/main/java/org/apache/sling/jcr/contentloader/internal/Loader.java @@ -0,0 +1,1040 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.sling.jcr.contentloader.internal; + +import static javax.jcr.ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.jcr.InvalidSerializedDataException; +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.osgi.framework.Bundle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The <code>Loader</code> loads initial content from the bundle. + */ +public class Loader { + + public static final String EXT_XML = ".xml"; + + public static final String EXT_JCR_XML = ".jcr.xml"; + + public static final String EXT_JSON = ".json"; + + public static final String ROOT_DESCRIPTOR = "/ROOT"; + + // default content type for createFile() + private static final String DEFAULT_CONTENT_TYPE = "application/octet-stream"; + + /** default log */ + private final Logger log = LoggerFactory.getLogger(Loader.class); + + private ContentLoaderService jcrContentHelper; + + private Map<String, ImportProvider> importProviders; + + private Map<String, List<String>> delayedReferences; + + // bundles whose registration failed and should be retried + private List<Bundle> delayedBundles; + + public Loader(ContentLoaderService jcrContentHelper) { + this.jcrContentHelper = jcrContentHelper; + this.delayedReferences = new HashMap<String, List<String>>(); + this.delayedBundles = new LinkedList<Bundle>(); + + importProviders = new LinkedHashMap<String, ImportProvider>(); + importProviders.put(EXT_JCR_XML, null); + importProviders.put(EXT_JSON, JsonReader.PROVIDER); + importProviders.put(EXT_XML, XmlReader.PROVIDER); + } + + public void dispose() { + delayedReferences = null; + if (delayedBundles != null) { + delayedBundles.clear(); + delayedBundles = null; + } + jcrContentHelper = null; + importProviders.clear(); + } + + /** + * Register a bundle and install its content. + * + * @param session + * @param bundle + */ + public void registerBundle(final Session session, final Bundle bundle, + final boolean isUpdate) { + + log.debug("Registering bundle {} for content loading.", + bundle.getSymbolicName()); + + if (registerBundleInternal(session, bundle, false, isUpdate)) { + + // handle delayed bundles, might help now + int currentSize = -1; + for (int i = delayedBundles.size(); i > 0 + && currentSize != delayedBundles.size() + && !delayedBundles.isEmpty(); i--) { + + for (Iterator<Bundle> di = delayedBundles.iterator(); di.hasNext();) { + + Bundle delayed = di.next(); + if (registerBundleInternal(session, delayed, true, false)) { + di.remove(); + } + + } + + currentSize = delayedBundles.size(); + } + + } else if (!isUpdate) { + // add to delayed bundles - if this is not an update! + delayedBundles.add(bundle); + } + } + + private boolean registerBundleInternal(final Session session, + final Bundle bundle, final boolean isRetry, final boolean isUpdate) { + + // check if bundle has initial content + final Iterator<PathEntry> pathIter = PathEntry.getContentPaths(bundle); + if (pathIter == null) { + log.debug("Bundle {} has no initial content", + bundle.getSymbolicName()); + return true; + } + + try { + + // check if the content has already been loaded + final Map<String, Object> bundleContentInfo = jcrContentHelper.getBundleContentInfo( + session, bundle); + + // if we don't get an info, someone else is currently loading + if (bundleContentInfo == null) { + return false; + } + + boolean success = false; + try { + + final boolean contentAlreadyLoaded = ((Boolean) bundleContentInfo.get(ContentLoaderService.PROPERTY_CONTENT_LOADED)).booleanValue(); + + if (!isUpdate && contentAlreadyLoaded) { + + log.info("Content of bundle already loaded {}.", + bundle.getSymbolicName()); + + } else { + + installContent(session, bundle, pathIter, + contentAlreadyLoaded); + + if (isRetry) { + // log success of retry + log.info( + "Retrytring to load initial content for bundle {} succeeded.", + bundle.getSymbolicName()); + } + + } + + success = true; + return true; + + } finally { + jcrContentHelper.unlockBundleContentInfo(session, bundle, + success); + } + + } catch (RepositoryException re) { + // if we are retrying we already logged this message once, so we + // won't log it again + if (!isRetry) { + log.error("Cannot load initial content for bundle " + + bundle.getSymbolicName() + " : " + re.getMessage(), re); + } + } + + return false; + } + + /** + * Unregister a bundle. Remove installed content. + * + * @param bundle The bundle. + */ + public void unregisterBundle(final Session session, final Bundle bundle) { + + // check if bundle has initial content + final Iterator<PathEntry> pathIter = PathEntry.getContentPaths(bundle); + if (delayedBundles.contains(bundle)) { + + delayedBundles.remove(bundle); + + } else { + + if (pathIter != null) { + uninstallContent(session, bundle, pathIter); + jcrContentHelper.contentIsUninstalled(session, bundle); + } + + } + } + + // ---------- internal ----------------------------------------------------- + + private void installContent(final Session session, final Bundle bundle, + final Iterator<PathEntry> pathIter, + final boolean contentAlreadyLoaded) throws RepositoryException { + log.debug("Installing initial content from bundle {}", + bundle.getSymbolicName()); + try { + + // the nodes marked to be checked-in after import + List<Node> versionables = new ArrayList<Node>(); + + while (pathIter.hasNext()) { + final PathEntry entry = pathIter.next(); + if (!contentAlreadyLoaded || entry.isOverwrite()) { + + Node targetNode = getTargetNode(session, entry.getTarget()); + + if (targetNode != null) { + installFromPath(bundle, entry.getPath(), + entry.isOverwrite(), versionables, + entry.isCheckin(), targetNode); + } + } + } + + // persist modifications now + session.save(); + + // finally checkin versionable nodes + for (Node versionable : versionables) { + versionable.checkin(); + } + + } finally { + try { + if (session.hasPendingChanges()) { + session.refresh(false); + } + } catch (RepositoryException re) { + log.warn( + "Failure to rollback partial initial content for bundle {}", + bundle.getSymbolicName(), re); + } + } + log.debug("Done installing initial content from bundle {}", + bundle.getSymbolicName()); + + } + + /** + * Handle content installation for a single path. + * + * @param bundle The bundle containing the content. + * @param path The path + * @param overwrite Should the content be overwritten. + * @param parent The parent node. + * @throws RepositoryException + */ + private void installFromPath(final Bundle bundle, final String path, + final boolean overwrite, List<Node> versionables, + final boolean checkin, final Node parent) + throws RepositoryException { + + @SuppressWarnings("unchecked") + Enumeration<String> entries = bundle.getEntryPaths(path); + if (entries == null) { + log.info("install: No initial content entries at {}", path); + return; + } + + Set<URL> ignoreEntry = new HashSet<URL>(); + + // potential root node import/extension + URL rootNodeDescriptor = importRootNode(parent.getSession(), bundle, + path, versionables, checkin); + if (rootNodeDescriptor != null) { + ignoreEntry.add(rootNodeDescriptor); + } + + while (entries.hasMoreElements()) { + final String entry = entries.nextElement(); + log.debug("Processing initial content entry {}", entry); + if (entry.endsWith("/")) { + + // dir, check for node descriptor , else create dir + String base = entry.substring(0, entry.length() - 1); + String name = getName(base); + + URL nodeDescriptor = null; + for (String ext : importProviders.keySet()) { + nodeDescriptor = bundle.getEntry(base + ext); + if (nodeDescriptor != null) { + break; + } + } + + // if we have a descriptor, which has not been processed yet, + // otherwise call createFolder, which creates an nt:folder or + // returns an existing node (created by a descriptor) + Node node = null; + if (nodeDescriptor != null + && !ignoreEntry.contains(nodeDescriptor)) { + node = createNode(parent, name, nodeDescriptor, overwrite, + versionables, checkin); + ignoreEntry.add(nodeDescriptor); + } else { + node = createFolder(parent, name, overwrite); + } + + // walk down the line + if (node != null) { + installFromPath(bundle, entry, overwrite, versionables, + checkin, node); + } + + } else { + + // file => create file + URL file = bundle.getEntry(entry); + if (ignoreEntry.contains(file)) { + // this is a consumed node descriptor + continue; + } + + // install if it is a descriptor + boolean foundProvider = false; + final Iterator<String> ipIter = importProviders.keySet().iterator(); + while (!foundProvider && ipIter.hasNext()) { + final String ext = ipIter.next(); + if (entry.endsWith(ext)) { + foundProvider = true; + } + } + if (foundProvider) { + if (createNode(parent, getName(entry), file, overwrite, + versionables, checkin) != null) { + ignoreEntry.add(file); + continue; + } + } + + // otherwise just place as file + try { + createFile(parent, file); + } catch (IOException ioe) { + log.warn("Cannot create file node for {}", file, ioe); + } + } + } + } + + private Node createNode(Node parent, String name, URL nodeXML, + boolean overwrite, List<Node> versionables, boolean checkin) + throws RepositoryException { + + InputStream ins = null; + try { + // special treatment for system view imports + if (nodeXML.getPath().toLowerCase().endsWith(EXT_JCR_XML)) { + return importSystemView(parent, name, nodeXML); + } + + NodeReader nodeReader = null; + for (Map.Entry<String, ImportProvider> e : importProviders.entrySet()) { + if (nodeXML.getPath().toLowerCase().endsWith(e.getKey())) { + nodeReader = e.getValue().getReader(); + break; + } + } + + // cannot find out the type + if (nodeReader == null) { + return null; + } + + ins = nodeXML.openStream(); + NodeDescription clNode = nodeReader.parse(ins); + + // nothing has been parsed + if (clNode == null) { + return null; + } + + if (clNode.getName() == null) { + // set the name without the [last] extension (xml or json) + clNode.setName(toPlainName(name)); + } + + return createNode(parent, clNode, overwrite, versionables, checkin); + } catch (RepositoryException re) { + throw re; + } catch (Throwable t) { + throw new RepositoryException(t.getMessage(), t); + } finally { + if (ins != null) { + try { + ins.close(); + } catch (IOException ignore) { + } + } + } + } + + /** + * Delete the node from the initial content. + * + * @param parent + * @param name + * @param nodeXML + * @throws RepositoryException + */ + private void deleteNode(Node parent, String name) + throws RepositoryException { + if (parent.hasNode(name)) { + parent.getNode(name).remove(); + } + } + + private Node createNode(Node parentNode, NodeDescription clNode, + final boolean overwrite, List<Node> versionables, boolean checkin) + throws RepositoryException { + + // if node already exists but should be overwritten, delete it + if (overwrite && parentNode.hasNode(clNode.getName())) { + parentNode.getNode(clNode.getName()).remove(); + } + + // ensure repository node + Node node; + if (parentNode.hasNode(clNode.getName())) { + + // use existing node + node = parentNode.getNode(clNode.getName()); + + } else if (clNode.getPrimaryNodeType() == null) { + + // node explicit node type, use repository default + node = parentNode.addNode(clNode.getName()); + + } else { + + // explicit primary node type + node = parentNode.addNode(clNode.getName(), + clNode.getPrimaryNodeType()); + } + + return setupNode(node, clNode, versionables, checkin); + } + + private Node setupNode(Node node, NodeDescription clNode, + List<Node> versionables, boolean checkin) + throws RepositoryException { + + // ammend mixin node types + if (clNode.getMixinNodeTypes() != null) { + for (String mixin : clNode.getMixinNodeTypes()) { + if (!node.isNodeType(mixin)) { + node.addMixin(mixin); + } + } + } + + // check if node is versionable + boolean addToVersionables = checkin + && node.isNodeType("mix:versionable"); + + if (clNode.getProperties() != null) { + for (PropertyDescription prop : clNode.getProperties()) { + if (node.hasProperty(prop.getName()) + && !node.getProperty(prop.getName()).isNew()) { + continue; + } + + int type = PropertyType.valueFromName(prop.getType()); + if (prop.isMultiValue()) { + + String[] values = prop.getValues().toArray( + new String[prop.getValues().size()]); + node.setProperty(prop.getName(), values, type); + + } else if (type == PropertyType.REFERENCE) { + + // need to resolve the reference + String propPath = node.getPath() + "/" + prop.getName(); + String uuid = getUUID(node.getSession(), propPath, + prop.getValue()); + if (uuid != null) { + node.setProperty(prop.getName(), uuid, type); + } + + } else if ("jcr:isCheckedOut".equals(prop.getName())) { + + // don't try to write the property but record its state + // for later checkin if set to false + boolean checkedout = Boolean.valueOf(prop.getValue()); + if (!checkedout) { + addToVersionables = true; + } + + } else { + + node.setProperty(prop.getName(), prop.getValue(), type); + + } + } + } + + // add the current node to the list of versionables to be checked + // in at the end. This is done if checkin is true and the node is + // versionable or if the jcr:isCheckedOut property is false + if (addToVersionables) { + versionables.add(node); + } + + // create child nodes from the descriptor + if (clNode.getChildren() != null) { + for (NodeDescription child : clNode.getChildren()) { + createNode(node, child, false, versionables, checkin); + } + } + + // resolve REFERENCE property values pointing to this node + resolveReferences(node); + + return node; + } + + /** + * Create a folder + * + * @param parent The parent node. + * @param name The name of the folder + * @param overwrite If set to true, an existing folder is removed first. + * @return The node pointing to the folder. + * @throws RepositoryException + */ + private Node createFolder(Node parent, String name, final boolean overwrite) + throws RepositoryException { + if (parent.hasNode(name)) { + if (overwrite) { + parent.getNode(name).remove(); + } else { + return parent.getNode(name); + } + } + + return parent.addNode(name, "nt:folder"); + } + + /** + * Create a file from the given url. + * + * @param parent + * @param source + * @throws IOException + * @throws RepositoryException + */ + private void createFile(Node parent, URL source) throws IOException, + RepositoryException { + String name = getName(source.getPath()); + if (parent.hasNode(name)) { + return; + } + + URLConnection conn = source.openConnection(); + long lastModified = conn.getLastModified(); + String type = conn.getContentType(); + InputStream data = conn.getInputStream(); + + // ensure content type + if (type == null) { + type = jcrContentHelper.getMimeType(name); + if (type == null) { + log.info( + "createFile: Cannot find content type for {}, using {}", + source.getPath(), DEFAULT_CONTENT_TYPE); + type = DEFAULT_CONTENT_TYPE; + } + } + + // ensure sensible last modification date + if (lastModified <= 0) { + lastModified = System.currentTimeMillis(); + } + + Node file = parent.addNode(name, "nt:file"); + Node content = file.addNode("jcr:content", "nt:resource"); + content.setProperty("jcr:mimeType", type); + content.setProperty("jcr:lastModified", lastModified); + content.setProperty("jcr:data", data); + } + + /** + * Delete the file from the given url. + * + * @param parent + * @param source + * @throws IOException + * @throws RepositoryException + */ + private void deleteFile(Node parent, URL source) throws IOException, + RepositoryException { + String name = getName(source.getPath()); + if (parent.hasNode(name)) { + parent.getNode(name).remove(); + } + } + + private String getUUID(Session session, String propPath, + String referencePath) throws RepositoryException { + if (session.itemExists(referencePath)) { + Item item = session.getItem(referencePath); + if (item.isNode()) { + Node refNode = (Node) item; + if (refNode.isNodeType("mix:referenceable")) { + return refNode.getUUID(); + } + } + } else { + // not existing yet, keep for delayed setting + List<String> current = delayedReferences.get(referencePath); + if (current == null) { + current = new ArrayList<String>(); + delayedReferences.put(referencePath, current); + } + current.add(propPath); + } + + // no UUID found + return null; + } + + private void resolveReferences(Node node) throws RepositoryException { + List<String> props = delayedReferences.remove(node.getPath()); + if (props == null || props.size() == 0) { + return; + } + + // check whether we can set at all + if (!node.isNodeType("mix:referenceable")) { + return; + } + + Session session = node.getSession(); + String uuid = node.getUUID(); + + for (String property : props) { + String name = getName(property); + Node parentNode = getParentNode(session, property); + if (parentNode != null) { + parentNode.setProperty(name, uuid, PropertyType.REFERENCE); + } + } + } + + /** + * Gets and decods the name part of the <code>path</code>. The name is + * the part of the path after the last slash (or the complete path if no + * slash is contained). To support names containing unsupported characters + * such as colon (<code>:</code>), names may be URL encoded (see + * <code>java.net.URLEncoder</code>) using the <i>UTF-8</i> character + * encoding. In this case, this method decodes the name using the + * <code>java.netURLDecoder</code> class with the <i>UTF-8</i> character + * encoding. + * + * @param path The path from which to extract the name part. + * @return The URL decoded name part. + */ + private String getName(String path) { + int lastSlash = path.lastIndexOf('/'); + String name = (lastSlash < 0) ? path : path.substring(lastSlash + 1); + + // check for encoded characters (%xx) + // has encoded characters, need to decode + if (name.indexOf('%') >= 0) { + try { + return URLDecoder.decode(name, "UTF-8"); + } catch (UnsupportedEncodingException uee) { + // actually unexpected because UTF-8 is required by the spec + log.error("Cannot decode " + + name + + " beause the platform has no support for UTF-8, using undecoded"); + } catch (Exception e) { + // IllegalArgumentException or failure to decode + log.error("Cannot decode " + name + ", using undecoded", e); + } + } + + // not encoded or problems decoding, return the name unmodified + return name; + } + + private Node getParentNode(Session session, String path) + throws RepositoryException { + int lastSlash = path.lastIndexOf('/'); + + // not an absolute path, cannot find parent + if (lastSlash < 0) { + return null; + } + + // node below root + if (lastSlash == 0) { + return session.getRootNode(); + } + + // item in the hierarchy + path = path.substring(0, lastSlash); + if (!session.itemExists(path)) { + return null; + } + + Item item = session.getItem(path); + return (item.isNode()) ? (Node) item : null; + } + + private Node getTargetNode(Session session, String path) + throws RepositoryException { + + // not specyfied path directive + if (path == null) return session.getRootNode(); + + int firstSlash = path.indexOf("/"); + + // it´s a relative path + if (firstSlash != 0) path = "/" + path; + + Item item = session.getItem(path); + return (item.isNode()) ? (Node) item : null; + } + + private void uninstallContent(final Session session, final Bundle bundle, + final Iterator<PathEntry> pathIter) { + try { + log.debug("Uninstalling initial content from bundle {}", + bundle.getSymbolicName()); + while (pathIter.hasNext()) { + final PathEntry entry = pathIter.next(); + if (entry.isUninstall()) { + Node targetNode = getTargetNode(session, entry.getTarget()); + if (targetNode != null) + uninstallFromPath(bundle, entry.getPath(), targetNode); + } else { + log.debug( + "Ignoring to uninstall content at {}, uninstall directive is not set.", + entry.getPath()); + } + } + + // persist modifications now + session.save(); + log.debug("Done uninstalling initial content from bundle {}", + bundle.getSymbolicName()); + } catch (RepositoryException re) { + log.error("Unable to uninstall initial content from bundle " + + bundle.getSymbolicName(), re); + } finally { + try { + if (session.hasPendingChanges()) { + session.refresh(false); + } + } catch (RepositoryException re) { + log.warn( + "Failure to rollback uninstaling initial content for bundle {}", + bundle.getSymbolicName(), re); + } + } + } + + /** + * Handle content uninstallation for a single path. + * + * @param bundle The bundle containing the content. + * @param path The path + * @param parent The parent node. + * @throws RepositoryException + */ + private void uninstallFromPath(final Bundle bundle, final String path, + final Node parent) throws RepositoryException { + @SuppressWarnings("unchecked") + Enumeration<String> entries = bundle.getEntryPaths(path); + if (entries == null) { + return; + } + + Set<URL> ignoreEntry = new HashSet<URL>(); + + // potential root node import/extension + Descriptor rootNodeDescriptor = getRootNodeDescriptor(bundle, path); + if (rootNodeDescriptor != null) { + ignoreEntry.add(rootNodeDescriptor.rootNodeDescriptor); + } + + while (entries.hasMoreElements()) { + final String entry = entries.nextElement(); + log.debug("Processing initial content entry {}", entry); + if (entry.endsWith("/")) { + // dir, check for node descriptor , else create dir + String base = entry.substring(0, entry.length() - 1); + String name = getName(base); + + URL nodeDescriptor = null; + for (String ext : importProviders.keySet()) { + nodeDescriptor = bundle.getEntry(base + ext); + if (nodeDescriptor != null) { + break; + } + } + + final Node node; + boolean delete = false; + if (nodeDescriptor != null + && !ignoreEntry.contains(nodeDescriptor)) { + node = (parent.hasNode(toPlainName(name)) + ? parent.getNode(toPlainName(name)) + : null); + delete = true; + } else { + node = (parent.hasNode(name) ? parent.getNode(name) : null); + } + + if (node != null) { + // walk down the line + uninstallFromPath(bundle, entry, node); + } + + if (delete) { + deleteNode(parent, toPlainName(name)); + ignoreEntry.add(nodeDescriptor); + } + + } else { + // file => create file + URL file = bundle.getEntry(entry); + if (ignoreEntry.contains(file)) { + // this is a consumed node descriptor + continue; + } + + // uninstall if it is a descriptor + boolean foundProvider = false; + final Iterator<String> ipIter = importProviders.keySet().iterator(); + while (!foundProvider && ipIter.hasNext()) { + final String ext = ipIter.next(); + if (entry.endsWith(ext)) { + foundProvider = true; + } + } + if (foundProvider) { + deleteNode(parent, toPlainName(getName(entry))); + ignoreEntry.add(file); + continue; + } + + // otherwise just delete the file + try { + deleteFile(parent, file); + } catch (IOException ioe) { + log.warn("Cannot delete file node for {}", file, ioe); + } + } + } + } + + /** + * Import the XML file as JCR system or document view import. If the XML + * file is not a valid system or document view export/import file, + * <code>false</code> is returned. + * + * @param parent The parent node below which to import + * @param nodeXML The URL to the XML file to import + * @return <code>true</code> if the import succeeds, <code>false</code> + * if the import fails due to XML format errors. + * @throws IOException If an IO error occurrs reading the XML file. + */ + private Node importSystemView(Node parent, String name, URL nodeXML) + throws IOException { + + InputStream ins = null; + try { + + // check whether we have the content already, nothing to do then + name = toPlainName(name); + if (parent.hasNode(name)) { + log.debug( + "importSystemView: Node {} for XML {} already exists, nothing to to", + name, nodeXML); + return parent.getNode(name); + } + + ins = nodeXML.openStream(); + Session session = parent.getSession(); + session.importXML(parent.getPath(), ins, IMPORT_UUID_CREATE_NEW); + + // additionally check whether the expected child node exists + return (parent.hasNode(name)) ? parent.getNode(name) : null; + + } catch (InvalidSerializedDataException isde) { + + // the xml might not be System or Document View export, fall back + // to old-style XML reading + log.info( + "importSystemView: XML {} does not seem to be system view export, trying old style", + nodeXML); + return null; + + } catch (RepositoryException re) { + + // any other repository related issue... + log.info( + "importSystemView: Repository issue loading XML {}, trying old style", + nodeXML); + return null; + + } finally { + if (ins != null) { + try { + ins.close(); + } catch (IOException ignore) { + // ignore + } + } + } + + } + + protected static final class Descriptor { + public URL rootNodeDescriptor; + + public NodeReader nodeReader; + } + + /** + * Return the root node descriptor. + */ + private Descriptor getRootNodeDescriptor(final Bundle bundle, + final String path) { + URL rootNodeDescriptor = null; + + for (Map.Entry<String, ImportProvider> e : importProviders.entrySet()) { + if (e.getValue() != null) { + rootNodeDescriptor = bundle.getEntry(path + ROOT_DESCRIPTOR + + e.getKey()); + if (rootNodeDescriptor != null) { + try { + final Descriptor d = new Descriptor(); + d.rootNodeDescriptor = rootNodeDescriptor; + d.nodeReader = e.getValue().getReader(); + return d; + } catch (IOException ioe) { + log.error("Unable to setup node reader for " + + e.getKey(), ioe); + return null; + } + } + } + } + return null; + } + + /** + * Imports mixin nodes and properties (and optionally child nodes) of the + * root node. + */ + private URL importRootNode(Session session, Bundle bundle, String path, + List<Node> versionables, boolean checkin) + throws RepositoryException { + final Descriptor descriptor = getRootNodeDescriptor(bundle, path); + // no root descriptor found + if (descriptor == null) { + return null; + } + + InputStream ins = null; + try { + + ins = descriptor.rootNodeDescriptor.openStream(); + NodeDescription clNode = descriptor.nodeReader.parse(ins); + + setupNode(session.getRootNode(), clNode, versionables, checkin); + + return descriptor.rootNodeDescriptor; + } catch (RepositoryException re) { + throw re; + } catch (Throwable t) { + throw new RepositoryException(t.getMessage(), t); + } finally { + if (ins != null) { + try { + ins.close(); + } catch (IOException ignore) { + } + } + } + + } + + private String toPlainName(String name) { + String providerExt = null; + final Iterator<String> ipIter = importProviders.keySet().iterator(); + while (providerExt == null && ipIter.hasNext()) { + final String ext = ipIter.next(); + if (name.endsWith(ext)) { + providerExt = ext; + } + } + if (providerExt != null) { + return name.substring(0, name.length() - providerExt.length()); + } + return name; + + } +} diff --git a/src/main/java/org/apache/sling/jcr/contentloader/internal/NodeDescription.java b/src/main/java/org/apache/sling/jcr/contentloader/internal/NodeDescription.java new file mode 100644 index 0000000..043da37 --- /dev/null +++ b/src/main/java/org/apache/sling/jcr/contentloader/internal/NodeDescription.java @@ -0,0 +1,153 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.sling.jcr.contentloader.internal; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +class NodeDescription { + + private String name; + private String primaryNodeType; + private Set<String> mixinNodeTypes; + private List<PropertyDescription> properties; + private List<NodeDescription> children; + + /** + * @return the children + */ + List<NodeDescription> getChildren() { + return children; + } + + /** + * @param children the children to set + */ + void addChild(NodeDescription child) { + if (child != null) { + if (children == null) { + children = new ArrayList<NodeDescription>(); + } + + children.add(child); + } + } + + /** + * @return the mixinNodeTypes + */ + Set<String> getMixinNodeTypes() { + return mixinNodeTypes; + } + + /** + * @param mixinNodeTypes the mixinNodeTypes to set + */ + void addMixinNodeType(String mixinNodeType) { + if (mixinNodeType != null && mixinNodeType.length() > 0) { + if (mixinNodeTypes == null) { + mixinNodeTypes = new HashSet<String>(); + } + + mixinNodeTypes.add(mixinNodeType); + } + } + + /** + * @return the name + */ + String getName() { + return name; + } + + /** + * @param name the name to set + */ + void setName(String name) { + this.name = name; + } + + /** + * @return the primaryNodeType + */ + String getPrimaryNodeType() { + return primaryNodeType; + } + + /** + * @param primaryNodeType the primaryNodeType to set + */ + void setPrimaryNodeType(String primaryNodeType) { + this.primaryNodeType = primaryNodeType; + } + + /** + * @return the properties + */ + List<PropertyDescription> getProperties() { + return properties; + } + + /** + * @param properties the properties to set + */ + void addProperty(PropertyDescription property) { + if (property != null) { + if (properties == null) { + properties = new ArrayList<PropertyDescription>(); + } + + properties.add(property); + } + } + + public int hashCode() { + int code = getName().hashCode() * 17; + if (getPrimaryNodeType() != null) { + code += getPrimaryNodeType().hashCode(); + } + return code; + } + + public boolean equals(Object obj) { + if (obj == this) { + return true; + } else if (!(obj instanceof NodeDescription)) { + return false; + } + + NodeDescription other = (NodeDescription) obj; + return getName().equals(other.getName()) + && equals(getPrimaryNodeType(), other.getPrimaryNodeType()) + && equals(getMixinNodeTypes(), other.getMixinNodeTypes()) + && equals(getProperties(), other.getProperties()) + && equals(getChildren(), other.getChildren()); + } + + public String toString() { + return "Node " + getName() + ", primary=" + getPrimaryNodeType() + + ", mixins=" + getMixinNodeTypes(); + } + + private boolean equals(Object o1, Object o2) { + return (o1 == null) ? o2 == null : o1.equals(o2); + } +} diff --git a/src/main/java/org/apache/sling/jcr/contentloader/internal/NodeReader.java b/src/main/java/org/apache/sling/jcr/contentloader/internal/NodeReader.java new file mode 100644 index 0000000..2653114 --- /dev/null +++ b/src/main/java/org/apache/sling/jcr/contentloader/internal/NodeReader.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.sling.jcr.contentloader.internal; + +import java.io.IOException; +import java.io.InputStream; + +/** + * The <code>NodeReader</code> TODO + */ +interface NodeReader { + + NodeDescription parse(InputStream ins) throws IOException; + +} diff --git a/src/main/java/org/apache/sling/jcr/contentloader/internal/PathEntry.java b/src/main/java/org/apache/sling/jcr/contentloader/internal/PathEntry.java new file mode 100644 index 0000000..cfb8c24 --- /dev/null +++ b/src/main/java/org/apache/sling/jcr/contentloader/internal/PathEntry.java @@ -0,0 +1,140 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.sling.jcr.contentloader.internal; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.apache.sling.commons.osgi.ManifestHeader; +import org.osgi.framework.Bundle; + +/** + * A path entry from the manifest for initial content. + */ +public class PathEntry { + + /** The manifest header to specify initial content to be loaded. */ + public static final String CONTENT_HEADER = "Sling-Initial-Content"; + + /** + * The overwrite directive specifying if content should be overwritten or + * just initially added. + */ + public static final String OVERWRITE_DIRECTIVE = "overwrite"; + + /** The uninstall directive specifying if content should be uninstalled. */ + public static final String UNINSTALL_DIRECTIVE = "uninstall"; + + /** + * The path directive specifying the target node where initial content will + * be loaded. + */ + public static final String PATH_DIRECTIVE = "path"; + + /** + * The checkin directive specifying whether versionable nodes should be + * checked in + */ + public static final String CHECKIN_DIRECTIVE = "checkin"; + + /** The path for the initial content. */ + private final String path; + + /** Should existing content be overwritten? */ + private final boolean overwrite; + + /** Should existing content be uninstalled? */ + private final boolean uninstall; + + /** Should versionable nodes be checked in? */ + private final boolean checkin; + + /** + * Target path where initial content will be loaded. If it´s null then + * target node is the root node + */ + private final String target; + + public static Iterator<PathEntry> getContentPaths(final Bundle bundle) { + final List<PathEntry> entries = new ArrayList<PathEntry>(); + + final String root = (String) bundle.getHeaders().get(CONTENT_HEADER); + if (root != null) { + final ManifestHeader header = ManifestHeader.parse(root); + for (final ManifestHeader.Entry entry : header.getEntries()) { + entries.add(new PathEntry(entry)); + } + } + + if (entries.size() == 0) { + return null; + } + return entries.iterator(); + } + + public PathEntry(ManifestHeader.Entry entry) { + // check for directives + final String overwriteValue = entry.getDirectiveValue(OVERWRITE_DIRECTIVE); + final String uninstallValue = entry.getDirectiveValue(UNINSTALL_DIRECTIVE); + final String pathValue = entry.getDirectiveValue(PATH_DIRECTIVE); + final String checkinValue = entry.getDirectiveValue(CHECKIN_DIRECTIVE); + boolean overwriteFlag = false; + if (overwriteValue != null) { + overwriteFlag = Boolean.valueOf(overwriteValue); + } + this.path = entry.getValue(); + this.overwrite = overwriteFlag; + if (uninstallValue != null) { + this.uninstall = Boolean.valueOf(uninstallValue); + } else { + this.uninstall = this.overwrite; + } + if (pathValue != null) { + this.target = pathValue; + } else { + this.target = null; + } + if (checkinValue != null) { + this.checkin = Boolean.valueOf(checkinValue); + } else { + this.checkin = false; + } + } + + public String getPath() { + return this.path; + } + + public boolean isOverwrite() { + return this.overwrite; + } + + public boolean isUninstall() { + return this.uninstall; + } + + public boolean isCheckin() { + return this.checkin; + } + + public String getTarget() { + return target; + } +} diff --git a/src/main/java/org/apache/sling/jcr/contentloader/internal/PropertyDescription.java b/src/main/java/org/apache/sling/jcr/contentloader/internal/PropertyDescription.java new file mode 100644 index 0000000..4fd7823 --- /dev/null +++ b/src/main/java/org/apache/sling/jcr/contentloader/internal/PropertyDescription.java @@ -0,0 +1,119 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.sling.jcr.contentloader.internal; + +import java.util.ArrayList; +import java.util.List; + +import javax.jcr.PropertyType; + +class PropertyDescription { + private String name; + private String value; + private List<String> values; + private String type = PropertyType.TYPENAME_STRING; // default type to string + + /** + * @return the name + */ + String getName() { + return this.name; + } + + /** + * @param name the name to set + */ + void setName(String name) { + this.name = name; + } + + /** + * @return the type + */ + String getType() { + return this.type; + } + + /** + * @param type the type to set + */ + void setType(String type) { + this.type = type; + } + + /** + * @return the value + */ + String getValue() { + return this.value; + } + + /** + * @param value the value to set + */ + void setValue(String value) { + this.value = value; + } + + /** + * @return the values + */ + List<String> getValues() { + return this.values; + } + + /** + * @param values the values to set + */ + void addValue(Object value) { + if (this.values == null) { + this.values = new ArrayList<String>(); + } + + if (value != null) { + this.values.add(value.toString()); + } + } + + boolean isMultiValue() { + return this.values != null; + } + + public int hashCode() { + return this.getName().hashCode() * 17 + this.getType().hashCode(); + } + + public boolean equals(Object obj) { + if (obj == this) { + return true; + } else if (!(obj instanceof PropertyDescription)) { + return false; + } + + PropertyDescription other = (PropertyDescription) obj; + return this.getName().equals(other.getName()) + && this.getType().equals(other.getType()) + && this.equals(this.getValues(), other.getValues()) + && this.equals(this.getValue(), other.getValue()); + } + + private boolean equals(Object o1, Object o2) { + return (o1 == null) ? o2 == null : o1.equals(o2); + } +} \ No newline at end of file diff --git a/src/main/java/org/apache/sling/jcr/contentloader/internal/XmlReader.java b/src/main/java/org/apache/sling/jcr/contentloader/internal/XmlReader.java new file mode 100644 index 0000000..a96155e --- /dev/null +++ b/src/main/java/org/apache/sling/jcr/contentloader/internal/XmlReader.java @@ -0,0 +1,169 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.sling.jcr.contentloader.internal; + +import java.io.IOException; +import java.io.InputStream; +import java.util.LinkedList; + +import org.kxml2.io.KXmlParser; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +public class XmlReader implements NodeReader { + + /* + * <node> <primaryNodeType>type</primaryNodeType> <mixinNodeTypes> + * <mixinNodeType>mixtype1</mixinNodeType> <mixinNodeType>mixtype2</mixinNodeType> + * </mixinNodeTypes> <properties> <property> <name>propName</name> + * <value>propValue</value> <type>propType</type> </property> <!-- more + * --> </properties> </node> + */ + + private static final String ELEM_NODE = "node"; + + private static final String ELEM_PRIMARY_NODE_TYPE = "primaryNodeType"; + + private static final String ELEM_MIXIN_NODE_TYPE = "mixinNodeType"; + + private static final String ELEM_PROPERTY = "property"; + + private static final String ELEM_NAME = "name"; + + private static final String ELEM_VALUE = "value"; + + private static final String ELEM_VALUES = "values"; + + private static final String ELEM_TYPE = "type"; + + static final ImportProvider PROVIDER = new ImportProvider() { + private XmlReader xmlReader; + + public NodeReader getReader() throws IOException { + if (xmlReader == null) { + try { + xmlReader = new XmlReader(); + } catch (Throwable t) { + throw (IOException) new IOException(t.getMessage()).initCause(t); + } + } + return xmlReader; + } + }; + + private KXmlParser xmlParser; + + XmlReader() { + this.xmlParser = new KXmlParser(); + } + + // ---------- XML content access ------------------------------------------- + + public synchronized NodeDescription parse(InputStream ins) throws IOException { + try { + return this.parseInternal(ins); + } catch (XmlPullParserException xppe) { + throw (IOException) new IOException(xppe.getMessage()).initCause(xppe); + } + } + + private NodeDescription parseInternal(InputStream ins) throws IOException, + XmlPullParserException { + String currentElement = "<root>"; + LinkedList<String> elements = new LinkedList<String>(); + NodeDescription currentNode = null; + LinkedList<NodeDescription> nodes = new LinkedList<NodeDescription>(); + StringBuffer contentBuffer = new StringBuffer(); + PropertyDescription currentProperty = null; + + // set the parser input, use null encoding to force detection with + // <?xml?> + this.xmlParser.setInput(ins, null); + + int eventType = this.xmlParser.getEventType(); + while (eventType != XmlPullParser.END_DOCUMENT) { + if (eventType == XmlPullParser.START_TAG) { + + elements.add(currentElement); + currentElement = this.xmlParser.getName(); + + if (ELEM_PROPERTY.equals(currentElement)) { + currentProperty = new PropertyDescription(); + } else if (ELEM_NODE.equals(currentElement)) { + if (currentNode != null) nodes.add(currentNode); + currentNode = new NodeDescription(); + } + + } else if (eventType == XmlPullParser.END_TAG) { + + String qName = this.xmlParser.getName(); + String content = contentBuffer.toString().trim(); + contentBuffer.delete(0, contentBuffer.length()); + + if (ELEM_PROPERTY.equals(qName)) { + currentNode.addProperty(currentProperty); + currentProperty = null; + + } else if (ELEM_NAME.equals(qName)) { + if (currentProperty != null) { + currentProperty.setName(content); + } else if (currentNode != null) { + currentNode.setName(content); + } + + } else if (ELEM_VALUE.equals(qName)) { + if (currentProperty.isMultiValue()) { + currentProperty.addValue(content); + } else { + currentProperty.setValue(content); + } + + } else if (ELEM_VALUES.equals(qName)) { + currentProperty.addValue(null); + currentProperty.setValue(null); + + } else if (ELEM_TYPE.equals(qName)) { + currentProperty.setType(content); + + } else if (ELEM_NODE.equals(qName)) { + if (!nodes.isEmpty()) { + NodeDescription parent = nodes.removeLast(); + parent.addChild(currentNode); + currentNode = parent; + } + + } else if (ELEM_PRIMARY_NODE_TYPE.equals(qName)) { + currentNode.setPrimaryNodeType(content); + + } else if (ELEM_MIXIN_NODE_TYPE.equals(qName)) { + currentNode.addMixinNodeType(content); + } + + currentElement = elements.removeLast(); + + } else if (eventType == XmlPullParser.TEXT) { + contentBuffer.append(this.xmlParser.getText()); + } + + eventType = this.xmlParser.next(); + } + + return currentNode; + } +} diff --git a/src/main/resources/META-INF/DISCLAIMER b/src/main/resources/META-INF/DISCLAIMER new file mode 100644 index 0000000..90850c2 --- /dev/null +++ b/src/main/resources/META-INF/DISCLAIMER @@ -0,0 +1,7 @@ +Apache Sling is an effort undergoing incubation at The Apache Software Foundation (ASF), +sponsored by the Apache Jackrabbit PMC. Incubation is required of all newly accepted +projects until a further review indicates that the infrastructure, communications, +and decision making process have stabilized in a manner consistent with other +successful ASF projects. While incubation status is not necessarily a reflection of +the completeness or stability of the code, it does indicate that the project has yet +to be fully endorsed by the ASF. \ No newline at end of file diff --git a/src/main/resources/META-INF/LICENSE b/src/main/resources/META-INF/LICENSE new file mode 100644 index 0000000..1a45730 --- /dev/null +++ b/src/main/resources/META-INF/LICENSE @@ -0,0 +1,232 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. + + +APACHE SLING SUBCOMPONENTS: + +Apache Sling includes subcomponents with separate copyright notices and +license terms. Your use of these subcomponents is subject to the terms +and conditions of the following licenses. + +kXML parser + + Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to permit + persons to whom the Software is furnished to do so, subject to the + following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + IN THE SOFTWARE. diff --git a/src/main/resources/META-INF/NOTICE b/src/main/resources/META-INF/NOTICE new file mode 100644 index 0000000..fdc2970 --- /dev/null +++ b/src/main/resources/META-INF/NOTICE @@ -0,0 +1,11 @@ +Apache Sling Initial Content Loader +Copyright 2008 The Apache Software Foundation + +Apache Sling is based on source code originally developed +by Day Software (http://www.day.com/). + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +This product includes software from http://kxml.sourceforge.net. +Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany. diff --git a/src/main/test/org/apache/sling/jcr/contentloader/internal/JsonReaderTest.java b/src/main/test/org/apache/sling/jcr/contentloader/internal/JsonReaderTest.java new file mode 100644 index 0000000..a975d3e --- /dev/null +++ b/src/main/test/org/apache/sling/jcr/contentloader/internal/JsonReaderTest.java @@ -0,0 +1,374 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.sling.jcr.contentloader.internal; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.jcr.PropertyType; + +import junit.framework.TestCase; + +import org.apache.sling.commons.json.JSONArray; +import org.apache.sling.commons.json.JSONException; +import org.apache.sling.commons.json.JSONObject; +import org.apache.sling.jcr.contentloader.internal.JsonReader; +import org.apache.sling.jcr.contentloader.internal.NodeDescription; +import org.apache.sling.jcr.contentloader.internal.PropertyDescription; + +public class JsonReaderTest extends TestCase { + + JsonReader jsonReader; + + protected void setUp() throws Exception { + super.setUp(); + this.jsonReader = new JsonReader(); + } + + protected void tearDown() throws Exception { + this.jsonReader = null; + super.tearDown(); + } + + public void testEmptyObject() { + try { + this.parse(""); + } catch (IOException ioe) { + fail("Expected IOException from empty JSON"); + } + } + + public void testEmpty() throws IOException { + NodeDescription node = this.parse("{}"); + assertNotNull("Expecting node", node); + assertNull("No name expected", node.getName()); + } + + public void testDefaultPrimaryNodeType() throws IOException { + String json = "{}"; + NodeDescription node = this.parse(json); + assertNotNull("Expecting node", node); + assertNull(node.getPrimaryNodeType()); + assertNull("No mixins expected", node.getMixinNodeTypes()); + assertNull("No properties expected", node.getProperties()); + assertNull("No children expected", node.getChildren()); + } + + public void testDefaultPrimaryNodeTypeWithSurroundWhitespace() throws IOException { + String json = " { } "; + NodeDescription node = this.parse(json); + assertNotNull("Expecting node", node); + assertNull(node.getPrimaryNodeType()); + assertNull("No mixins expected", node.getMixinNodeTypes()); + assertNull("No properties expected", node.getProperties()); + assertNull("No children expected", node.getChildren()); + } + + public void testDefaultPrimaryNodeTypeWithoutEnclosingBraces() throws IOException { + String json = ""; + NodeDescription node = this.parse(json); + assertNotNull("Expecting node", node); + assertNull(node.getPrimaryNodeType()); + assertNull("No mixins expected", node.getMixinNodeTypes()); + assertNull("No properties expected", node.getProperties()); + assertNull("No children expected", node.getChildren()); + } + + public void testDefaultPrimaryNodeTypeWithoutEnclosingBracesWithSurroundWhitespace() throws IOException { + String json = " "; + NodeDescription node = this.parse(json); + assertNotNull("Expecting node", node); + assertNull(node.getPrimaryNodeType()); + assertNull("No mixins expected", node.getMixinNodeTypes()); + assertNull("No properties expected", node.getProperties()); + assertNull("No children expected", node.getChildren()); + } + + public void testExplicitePrimaryNodeType() throws IOException { + String type = "xyz:testType"; + String json = "{ \"jcr:primaryType\": \"" + type + "\" }"; + + NodeDescription node = this.parse(json); + assertNotNull("Expecting node", node); + assertEquals(type, node.getPrimaryNodeType()); + } + + public void testMixinNodeTypes1() throws JSONException, IOException { + Set<Object> mixins = this.toSet(new Object[]{ "xyz:mix1" }); + String json = "{ \"jcr:mixinTypes\": " + this.toJsonArray(mixins) + "}"; + + NodeDescription node = this.parse(json); + assertNotNull("Expecting node", node); + assertEquals(mixins, node.getMixinNodeTypes()); + } + + public void testMixinNodeTypes2() throws JSONException, IOException { + Set<Object> mixins = this.toSet(new Object[]{ "xyz:mix1", "abc:mix2" }); + String json = "{ \"jcr:mixinTypes\": " + this.toJsonArray(mixins) + "}"; + + NodeDescription node = this.parse(json); + assertNotNull("Expecting node", node); + assertEquals(mixins, node.getMixinNodeTypes()); + } + + public void testPropertiesNone() throws IOException, JSONException { + List<PropertyDescription> properties = null; + String json = "{ \"properties\": " + this.toJsonObject(properties) + "}"; + + NodeDescription node = this.parse(json); + assertNotNull("Expecting node", node); + assertEquals(properties, node.getProperties()); + } + + public void testPropertiesSingleValue() throws IOException, JSONException { + List<PropertyDescription> properties = new ArrayList<PropertyDescription>(); + PropertyDescription prop = new PropertyDescription(); + prop.setName("p1"); + prop.setValue("v1"); + properties.add(prop); + + String json = this.toJsonObject(properties).toString(); + + NodeDescription node = this.parse(json); + assertNotNull("Expecting node", node); + assertEquals(new HashSet<PropertyDescription>(properties), new HashSet<PropertyDescription>(node.getProperties())); + } + + public void testPropertiesTwoSingleValue() throws IOException, JSONException { + List<PropertyDescription> properties = new ArrayList<PropertyDescription>(); + PropertyDescription prop = new PropertyDescription(); + prop.setName("p1"); + prop.setValue("v1"); + properties.add(prop); + prop = new PropertyDescription(); + prop.setName("p2"); + prop.setValue("v2"); + properties.add(prop); + + String json = this.toJsonObject(properties).toString(); + + NodeDescription node = this.parse(json); + assertNotNull("Expecting node", node); + assertEquals(new HashSet<PropertyDescription>(properties), new HashSet<PropertyDescription>(node.getProperties())); + } + + public void testPropertiesMultiValue() throws IOException, JSONException { + List<PropertyDescription> properties = new ArrayList<PropertyDescription>(); + PropertyDescription prop = new PropertyDescription(); + prop.setName("p1"); + prop.addValue("v1"); + properties.add(prop); + + String json = this.toJsonObject(properties).toString(); + + NodeDescription node = this.parse(json); + assertNotNull("Expecting node", node); + assertEquals(new HashSet<PropertyDescription>(properties), new HashSet<PropertyDescription>(node.getProperties())); + } + + public void testPropertiesMultiValueEmpty() throws IOException, JSONException { + List<PropertyDescription> properties = new ArrayList<PropertyDescription>(); + PropertyDescription prop = new PropertyDescription(); + prop.setName("p1"); + prop.addValue(null); // empty multivalue property + properties.add(prop); + + String json = this.toJsonObject(properties).toString(); + + NodeDescription node = this.parse(json); + assertNotNull("Expecting node", node); + assertEquals(new HashSet<PropertyDescription>(properties), new HashSet<PropertyDescription>(node.getProperties())); + } + + public void testChildrenNone() throws IOException, JSONException { + List<NodeDescription> nodes = null; + String json = this.toJsonObject(nodes).toString(); + + NodeDescription node = this.parse(json); + assertNotNull("Expecting node", node); + assertEquals(nodes, node.getChildren()); + } + + public void testChild() throws IOException, JSONException { + List<NodeDescription> nodes = new ArrayList<NodeDescription>(); + NodeDescription child = new NodeDescription(); + child.setName("p1"); + nodes.add(child); + + String json = this.toJsonObject(nodes).toString(); + + NodeDescription node = this.parse(json); + assertNotNull("Expecting node", node); + assertEquals(nodes, node.getChildren()); + } + + public void testChildWithMixin() throws IOException, JSONException { + List<NodeDescription> nodes = new ArrayList<NodeDescription>(); + NodeDescription child = new NodeDescription(); + child.setName("p1"); + child.addMixinNodeType("p1:mix"); + nodes.add(child); + + String json = this.toJsonObject(nodes).toString(); + + NodeDescription node = this.parse(json); + assertNotNull("Expecting node", node); + assertEquals(nodes, node.getChildren()); + } + + public void testTwoChildren() throws IOException, JSONException { + List<NodeDescription> nodes = new ArrayList<NodeDescription>(); + NodeDescription child = new NodeDescription(); + child.setName("p1"); + nodes.add(child); + child = new NodeDescription(); + child.setName("p2"); + nodes.add(child); + + String json = this.toJsonObject(nodes).toString(); + + NodeDescription node = this.parse(json); + assertNotNull("Expecting node", node); + assertEquals(nodes, node.getChildren()); + } + + public void testChildWithProperty() throws IOException, JSONException { + List<NodeDescription> nodes = new ArrayList<NodeDescription>(); + NodeDescription child = new NodeDescription(); + child.setName("c1"); + PropertyDescription prop = new PropertyDescription(); + prop.setName("c1p1"); + prop.setValue("c1v1"); + child.addProperty(prop); + nodes.add(child); + + String json = this.toJsonObject(nodes).toString(); + + NodeDescription node = this.parse(json); + assertNotNull("Expecting node", node); + assertEquals(nodes, node.getChildren()); + } + + //---------- internal helper ---------------------------------------------- + + private NodeDescription parse(String json) throws IOException { + String charSet = "ISO-8859-1"; + json = "#" + charSet + "\r\n" + json; + InputStream ins = new ByteArrayInputStream(json.getBytes(charSet)); + return this.jsonReader.parse(ins); + } + + private Set<Object> toSet(Object[] content) { + Set<Object> set = new HashSet<Object>(); + for (int i=0; content != null && i < content.length; i++) { + set.add(content[i]); + } + + return set; + } + + private JSONArray toJsonArray(Collection<?> set) throws JSONException { + List<Object> list = new ArrayList<Object>(); + for (Object item : set) { + if (item instanceof NodeDescription) { + list.add(this.toJsonObject((NodeDescription) item)); + } else { + list.add(item); + } + } + return new JSONArray(list); + } + + private JSONObject toJsonObject(Collection<?> set) throws JSONException { + JSONObject obj = new JSONObject(); + if (set != null) { + for (Object next: set) { + String name = this.getName(next); + obj.putOpt(name, this.toJsonObject(next)); + } + } + return obj; + } + + private Object toJsonObject(Object object) throws JSONException { + if (object instanceof NodeDescription) { + return this.toJsonObject((NodeDescription) object); + } else if (object instanceof PropertyDescription) { + return this.toJsonObject((PropertyDescription) object); + } + + // fall back to string representation + return String.valueOf(object); + } + + private JSONObject toJsonObject(NodeDescription node) throws JSONException { + JSONObject obj = new JSONObject(); + + if (node.getPrimaryNodeType() != null) { + obj.putOpt("jcr:primaryType", node.getPrimaryNodeType()); + } + + if (node.getMixinNodeTypes() != null) { + obj.putOpt("jcr:mixinTypes", this.toJsonArray(node.getMixinNodeTypes())); + } + + if (node.getProperties() != null) { + for (PropertyDescription prop : node.getProperties()) { + obj.put(prop.getName(), toJsonObject(prop)); + } + } + + if (node.getChildren() != null) { + for (NodeDescription child : node.getChildren()) { + obj.put(child.getName(), toJsonObject(child)); + } + } + + return obj; + } + + private Object toJsonObject(PropertyDescription property) throws JSONException { + if (!property.isMultiValue() && PropertyType.TYPENAME_STRING.equals(property.getType())) { + return this.toJsonObject(property.getValue()); + } + Object obj; + if (property.isMultiValue()) { + obj = this.toJsonArray(property.getValues()); + } else { + obj = this.toJsonObject(property.getValue()); + } + + return obj; + } + + private String getName(Object object) { + if (object instanceof NodeDescription) { + return ((NodeDescription) object).getName(); + } else if (object instanceof PropertyDescription) { + return ((PropertyDescription) object).getName(); + } + + // fall back to string representation + return String.valueOf(object); + } +} -- To stop receiving notification emails like this one, please contact "[email protected]" <[email protected]>.
