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 e7c3f36531d364510618407bd2ca8cf50d34d4a0 Author: Carsten Ziegeler <[email protected]> AuthorDate: Wed Apr 30 07:05:59 2008 +0000 SLING-400: Move content loading to own bundle. git-svn-id: https://svn.apache.org/repos/asf/incubator/sling/trunk/jcr/contentloader@652308 13f79535-47bb-0310-9956-ffa450edef68 --- pom.xml | 10 +- .../internal/ContentLoaderService.java | 327 ++++++++ .../jcr/contentloader/internal/ImportProvider.java | 27 + .../jcr/contentloader/internal/JsonReader.java | 190 +++++ .../sling/jcr/contentloader/internal/Loader.java | 924 +++++++++++++++++++++ .../contentloader/internal/NodeDescription.java | 153 ++++ .../jcr/contentloader/internal/NodeReader.java | 31 + .../jcr/contentloader/internal/PathEntry.java | 91 ++ .../internal/PropertyDescription.java | 119 +++ .../jcr/contentloader/internal/XmlReader.java | 169 ++++ 10 files changed, 2036 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index ef1f7e7..8b9958a 100644 --- a/pom.xml +++ b/pom.xml @@ -60,7 +60,7 @@ <configuration> <instructions> <Private-Package> - org.apache.sling.jcr.resource.internal.*, + org.apache.sling.jcr.contentloader.internal.*, org.kxml2.io, org.xmlpull.v1 </Private-Package> @@ -83,12 +83,13 @@ <artifactId>org.osgi.compendium</artifactId> </dependency> <dependency> - <groupId>javax.servlet</groupId> - <artifactId>servlet-api</artifactId> + <groupId>org.apache.sling</groupId> + <artifactId>org.apache.sling.jcr.api</artifactId> + <version>2.0.0-incubator-SNAPSHOT</version> </dependency> <dependency> <groupId>org.apache.sling</groupId> - <artifactId>org.apache.sling.api</artifactId> + <artifactId>org.apache.sling.commons.json</artifactId> <version>2.0.0-incubator-SNAPSHOT</version> </dependency> <dependency> @@ -100,7 +101,6 @@ <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> </dependency> - <dependency> <groupId>net.sf.kxml</groupId> <artifactId>kxml2</artifactId> 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..57874f3 --- /dev/null +++ b/src/main/java/org/apache/sling/jcr/contentloader/internal/ContentLoaderService.java @@ -0,0 +1,327 @@ +/* + * 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.Map; +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.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="false" + * @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; + + // ---------- 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(); + initialContentLoader.registerBundle(session, event.getBundle(), false); + } catch (Throwable t) { + log.error( + "bundleChanged: Problem loading initial content of bundle " + + event.getBundle().getSymbolicName() + " (" + + event.getBundle().getBundleId() + ")", t); + } + break; + case BundleEvent.UPDATED: + try { + Session session = getAdminSession(); + initialContentLoader.registerBundle(session, event.getBundle(), true); + } catch (Throwable t) { + log.error( + "bundleChanged: Problem updating initial content of bundle " + + event.getBundle().getSymbolicName() + " (" + + event.getBundle().getBundleId() + ")", t); + } + 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.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 unlockBundleContentInto(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", bundle.getBundleContext().getProperty("sling.id")); + 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", bundle.getBundleContext().getProperty("sling.id")); + 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..4b5a1e5 --- /dev/null +++ b/src/main/java/org/apache/sling/jcr/contentloader/internal/JsonReader.java @@ -0,0 +1,190 @@ +/* + * 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); + String name = json.optString("name", null); // allow for no name ! + return this.createNode(name, 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..300b2f8 --- /dev/null +++ b/src/main/java/org/apache/sling/jcr/contentloader/internal/Loader.java @@ -0,0 +1,924 @@ +/* + * 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() { + this.delayedReferences = null; + if (this.delayedBundles != null) { + this.delayedBundles.clear(); + this.delayedBundles = null; + } + this.jcrContentHelper = null; + this.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 (this.registerBundleInternal(session, bundle, false, isUpdate)) { + // handle delayed bundles, might help now + int currentSize = -1; + for (int i = this.delayedBundles.size(); i > 0 + && currentSize != this.delayedBundles.size() + && !this.delayedBundles.isEmpty(); i--) { + for (Iterator<Bundle> di = this.delayedBundles.iterator(); di.hasNext();) { + Bundle delayed = di.next(); + if (this.registerBundleInternal(session, delayed, true, false)) { + di.remove(); + } + } + currentSize = this.delayedBundles.size(); + } + } else { + // add to delayed bundles - if this is not an update! + if ( !isUpdate ) { + this.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 = this.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 { + this.installContent(session, bundle, pathIter); + if (isRetry) { + // log success of retry + log.info( + "Retrytring to load initial content for bundle {} succeeded.", + bundle.getSymbolicName()); + } + } + success = true; + return true; + } finally { + this.jcrContentHelper.unlockBundleContentInto(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 (this.delayedBundles.contains(bundle)) { + this.delayedBundles.remove(bundle); + } else { + if ( pathIter != null ) { + this.uninstallContent(session, bundle, pathIter); + this.jcrContentHelper.contentIsUninstalled(session, bundle); + } + } + } + + // ---------- internal ----------------------------------------------------- + + private void installContent(Session session, Bundle bundle, final Iterator<PathEntry> pathIter) + throws RepositoryException { + try { + log.debug("Installing initial content from bundle {}", + bundle.getSymbolicName()); + while (pathIter.hasNext() ) { + final PathEntry entry = pathIter.next(); + this.installFromPath(bundle, entry.getPath(), entry.isOverwrite(), session.getRootNode()); + } + + // persist modifications now + session.save(); + log.debug("Done installing initial content from bundle {}", + bundle.getSymbolicName()); + } finally { + try { + if (session.hasPendingChanges()) { + session.refresh(false); + } + } catch (RepositoryException re) { + log.warn( + "Failure to rollback partial initial content for bundle {}", + bundle.getSymbolicName(), re); + } + } + + } + + /** + * 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, + 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); + 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 = this.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 = this.createNode(parent, name, nodeDescriptor, overwrite); + ignoreEntry.add(nodeDescriptor); + } else { + node = this.createFolder(parent, name, overwrite); + } + + // walk down the line + if (node != null) { + this.installFromPath(bundle, entry, overwrite, 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 = this.importProviders.keySet().iterator(); + while ( !foundProvider && ipIter.hasNext() ) { + final String ext = ipIter.next(); + if ( entry.endsWith(ext) ) { + foundProvider = true; + } + } + if (foundProvider) { + if (this.createNode(parent, this.getName(entry), file, overwrite) != null) { + ignoreEntry.add(file); + continue; + } + } + + // otherwise just place as file + try { + this.createFile(parent, file); + } catch (IOException ioe) { + log.warn("Cannot create file node for {}", file, ioe); + } + } + } + } + + /** + * 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"); + } + + private Node createNode(Node parent, String name, URL nodeXML, boolean overwrite) + 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 this.createNode(parent, clNode, overwrite); + } 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) + 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); + } + + private Node setupNode(Node node, + NodeDescription clNode) + throws RepositoryException { + + // ammend mixin node types + if (clNode.getMixinNodeTypes() != null) { + for (String mixin : clNode.getMixinNodeTypes()) { + if (!node.isNodeType(mixin)) { + node.addMixin(mixin); + } + } + } + + 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 = this.getUUID(node.getSession(), propPath, + prop.getValue()); + if (uuid != null) { + node.setProperty(prop.getName(), uuid, type); + } + } else { + node.setProperty(prop.getName(), prop.getValue(), type); + } + } + } + + if (clNode.getChildren() != null) { + for (NodeDescription child : clNode.getChildren()) { + this.createNode(node, child, false); + } + } + + this.resolveReferences(node); + + return node; + } + + /** + * 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 = this.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 = this.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 = this.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 = this.delayedReferences.get(referencePath); + if (current == null) { + current = new ArrayList<String>(); + this.delayedReferences.put(referencePath, current); + } + current.add(propPath); + } + + // no UUID found + return null; + } + + private void resolveReferences(Node node) throws RepositoryException { + List<String> props = this.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 = this.getName(property); + Node parentNode = this.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 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.isOverwrite() ) { + this.uninstallFromPath(bundle, entry.getPath(), session.getRootNode()); + } else { + log.debug("Ignoring to uninstall content at {}, overwrite flag 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 = this.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 = this.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 + this.uninstallFromPath(bundle, entry, node); + } + + if (delete) { + this.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 = this.importProviders.keySet().iterator(); + while ( !foundProvider && ipIter.hasNext() ) { + final String ext = ipIter.next(); + if ( entry.endsWith(ext) ) { + foundProvider = true; + } + } + if (foundProvider) { + this.deleteNode(parent, toPlainName(this.getName(entry))); + ignoreEntry.add(file); + continue; + } + + // otherwise just delete the file + try { + this.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) { + this.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) + throws RepositoryException { + final Descriptor descriptor = this.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); + + 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 = this.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..b9a0633 --- /dev/null +++ b/src/main/java/org/apache/sling/jcr/contentloader/internal/PathEntry.java @@ -0,0 +1,91 @@ +/* + * 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 java.util.StringTokenizer; + +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 flag specifying if content should be overwritten or just initially added. */ + public static final String OVERWRITE_FLAG = "overwrite"; + + /** The path for the initial content. */ + private final String path; + + /** Should existing content be overwritten? */ + private final boolean overwrite; + + 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 StringTokenizer tokener = new StringTokenizer(root, ","); + while (tokener.hasMoreTokens()) { + final String path = tokener.nextToken().trim(); + entries.add(new PathEntry(path)); + } + } + + if ( entries.size() == 0 ) { + return null; + } + return entries.iterator(); + } + + public PathEntry(String path) { + // check for overwrite flag + boolean overwrite = false; + int flagPos = path.indexOf(";"); + if ( flagPos != -1 ) { + final StringTokenizer flagTokenizer = new StringTokenizer(path.substring(flagPos+1), ";"); + while ( flagTokenizer.hasMoreTokens() ) { + final String token = flagTokenizer.nextToken(); + int pos = token.indexOf(":="); + if ( pos != -1 ) { + if ( token.substring(0, pos).equals(OVERWRITE_FLAG) ) { + overwrite = Boolean.valueOf(token.substring(pos+2)); + } + } + } + path = path.substring(0, flagPos); + } + this.path = path; + this.overwrite = overwrite; + } + + public String getPath() { + return this.path; + } + + public boolean isOverwrite() { + return this.overwrite; + } +} 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; + } +} -- To stop receiving notification emails like this one, please contact "[email protected]" <[email protected]>.
