| /* |
| * 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.api.resource; |
| |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.LinkedHashMap; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import javax.annotation.CheckForNull; |
| import javax.annotation.Nonnull; |
| |
| /** |
| * The <code>ResourceMetadata</code> interface defines the API for the |
| * metadata of a Sling {@link Resource}. Essentially the resource's metadata is |
| * just a map of objects indexed by string keys. |
| * <p> |
| * The actual contents of the meta data map is implementation specific with the |
| * exception of the {@link #RESOLUTION_PATH sling.resolutionPath} property which |
| * must be provided by all implementations and contain the part of the request |
| * URI used to resolve the resource. The type of this property value is defined |
| * to be <code>String</code>. |
| * <p> |
| * Note, that the prefix <em>sling.</em> to key names is reserved for the |
| * Sling implementation. |
| * |
| * Once a resource is returned by the {@link ResourceResolver}, the resource |
| * metadata is made read-only and therefore can't be changed by client code! |
| */ |
| public class ResourceMetadata extends HashMap<String, Object> { |
| |
| private static final long serialVersionUID = 4692666752269523738L; |
| |
| /** |
| * The name of the required property providing the part of the request URI |
| * which was used to the resolve the resource to which the meta data |
| * instance belongs (value is "sling.resolutionPath"). |
| */ |
| public static final String RESOLUTION_PATH = "sling.resolutionPath"; |
| |
| /** |
| * The name of the required property providing the part of the request URI |
| * which was not used to the resolve the resource to which the meta data |
| * instance belongs (value is "sling.resolutionPathInfo"). The value of this |
| * property concatenated to the value of the |
| * {@link #RESOLUTION_PATH sling.resolutionPath} property returns the |
| * original request URI leading to the resource. |
| * <p> |
| * This property is optional. If missing, it should be assumed equal to an |
| * empty string. |
| * |
| * @since 2.0.4 (Sling API Bundle 2.0.4) |
| */ |
| public static final String RESOLUTION_PATH_INFO = "sling.resolutionPathInfo"; |
| |
| /** |
| * The name of the optional property providing the content type of the |
| * resource if the resource is streamable (value is "sling.contentType"). |
| * This property may be missing if the resource is not streamable or if the |
| * content type is not known. |
| */ |
| public static final String CONTENT_TYPE = "sling.contentType"; |
| |
| /** |
| * The name of the optional property providing the content length of the |
| * resource if the resource is streamable (value is "sling.contentLength"). |
| * This property may be missing if the resource is not streamable or if the |
| * content length is not known. |
| * <p> |
| * Note, that unlike the other properties, this property may be set only |
| * after the resource has successfully been adapted to an |
| * <code>InputStream</code> for performance reasons. |
| */ |
| public static final String CONTENT_LENGTH = "sling.contentLength"; |
| |
| /** |
| * The name of the optional property providing the character encoding of the |
| * resource if the resource is streamable and contains character data (value |
| * is "sling.characterEncoding"). This property may be missing if the |
| * resource is not streamable or if the character encoding is not known. |
| */ |
| public static final String CHARACTER_ENCODING = "sling.characterEncoding"; |
| |
| /** |
| * Returns the creation time of this resource in the repository in |
| * milliseconds (value is "sling.creationTime"). The type of this property |
| * is <code>java.lang.Long</code>. The property may be missing if the |
| * resource is not streamable or if the creation time is not known. |
| */ |
| public static final String CREATION_TIME = "sling.creationTime"; |
| |
| /** |
| * Returns the last modification time of this resource in the repository in |
| * milliseconds (value is "sling.modificationTime"). The type of this |
| * property is <code>java.lang.Long</code>. The property may be missing |
| * if the resource is not streamable or if the last modification time is not |
| * known. |
| */ |
| public static final String MODIFICATION_TIME = "sling.modificationTime"; |
| |
| /** |
| * Returns whether the resource resolver should continue to search for a |
| * resource. |
| * A resource provider can set this flag to indicate that the resource |
| * resolver should search for a provider with a lower priority. If it |
| * finds a resource using such a provider, that resource is returned |
| * instead. If none is found this resource is returned. |
| * This flag should never be manipulated by application code! |
| * The value of this property has no meaning, the resource resolver |
| * just checks whether this flag is set or not. |
| * @since 2.2 (Sling API Bundle 2.2.0) |
| * @deprecated This flag is not supported anymore when implementing the SPI |
| * based {@code org.apache.sling.spi.resource.provider.ResourceProvider} |
| */ |
| public static final String INTERNAL_CONTINUE_RESOLVING = ":org.apache.sling.resource.internal.continue.resolving"; |
| |
| /** |
| * Returns a map containing parameters added to path after semicolon. |
| * For instance, map for path <code>/content/test;v='1.2.3'.html</code> |
| * will contain one entry key <code>v</code> and value <code>1.2.3</code>. |
| */ |
| public static final String PARAMETER_MAP = "sling.parameterMap"; |
| |
| private boolean isReadOnly = false; |
| |
| /** |
| * Sets the {@link #CHARACTER_ENCODING} property to <code>encoding</code> |
| * if not <code>null</code>. |
| * @param encoding The encoding |
| */ |
| public void setCharacterEncoding(String encoding) { |
| if (encoding != null) { |
| put(CHARACTER_ENCODING, encoding); |
| } |
| } |
| |
| /** |
| * Returns the {@link #CHARACTER_ENCODING} property if not <code>null</code> |
| * and a <code>String</code> instance. Otherwise <code>null</code> is |
| * returned. |
| * @return The character encoding |
| */ |
| public @CheckForNull String getCharacterEncoding() { |
| Object value = get(CHARACTER_ENCODING); |
| if (value instanceof String) { |
| return (String) value; |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Sets the {@link #CONTENT_TYPE} property to <code>contentType</code> if |
| * not <code>null</code>. |
| * @param contentType The content type |
| */ |
| public void setContentType(String contentType) { |
| if (contentType != null) { |
| put(CONTENT_TYPE, contentType); |
| } |
| } |
| |
| /** |
| * Returns the {@link #CONTENT_TYPE} property if not <code>null</code> and |
| * a <code>String</code> instance. Otherwise <code>null</code> is |
| * returned. |
| * @return The content type |
| */ |
| public @CheckForNull String getContentType() { |
| Object value = get(CONTENT_TYPE); |
| if (value instanceof String) { |
| return (String) value; |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Sets the {@link #CONTENT_LENGTH} property to <code>contentType</code> |
| * if not <code>null</code>. |
| * @param contentLength The content length |
| */ |
| public void setContentLength(long contentLength) { |
| if (contentLength > 0) { |
| put(CONTENT_LENGTH, contentLength); |
| } |
| } |
| |
| /** |
| * Returns the {@link #CONTENT_LENGTH} property if not <code>null</code> |
| * and a <code>long</code>. Otherwise <code>-1</code> is returned. |
| * @return The content length |
| */ |
| public long getContentLength() { |
| Object value = get(CONTENT_LENGTH); |
| if (value instanceof Long) { |
| return (Long) value; |
| } |
| |
| return -1; |
| } |
| |
| /** |
| * Sets the {@link #CREATION_TIME} property to <code>creationTime</code> |
| * if not negative. |
| * @param creationTime The creation time |
| */ |
| public void setCreationTime(long creationTime) { |
| if (creationTime >= 0) { |
| put(CREATION_TIME, creationTime); |
| } |
| } |
| |
| /** |
| * Returns the {@link #CREATION_TIME} property if not <code>null</code> |
| * and a <code>long</code>. Otherwise <code>-1</code> is returned. |
| * @return The creation time |
| */ |
| public long getCreationTime() { |
| Object value = get(CREATION_TIME); |
| if (value instanceof Long) { |
| return (Long) value; |
| } |
| |
| return -1; |
| } |
| |
| /** |
| * Sets the {@link #MODIFICATION_TIME} property to |
| * <code>modificationTime</code> if not negative. |
| * @param modificationTime The modification time |
| */ |
| public void setModificationTime(long modificationTime) { |
| if (modificationTime >= 0) { |
| put(MODIFICATION_TIME, modificationTime); |
| } |
| } |
| |
| /** |
| * Returns the {@link #MODIFICATION_TIME} property if not <code>null</code> |
| * and a <code>long</code>. Otherwise <code>-1</code> is returned. |
| * @return The modification time |
| */ |
| public long getModificationTime() { |
| Object value = get(MODIFICATION_TIME); |
| if (value instanceof Long) { |
| return (Long) value; |
| } |
| |
| return -1; |
| } |
| |
| /** |
| * Sets the {@link #RESOLUTION_PATH} property to <code>resolutionPath</code> |
| * if not <code>null</code>. |
| * @param resolutionPath The resolution path |
| */ |
| public void setResolutionPath(String resolutionPath) { |
| if (resolutionPath != null) { |
| put(RESOLUTION_PATH, resolutionPath); |
| } |
| } |
| |
| /** |
| * Returns the {@link #RESOLUTION_PATH} property if not <code>null</code> |
| * and a <code>String</code> instance. Otherwise <code>null</code> is |
| * returned. |
| * @return The resolution path |
| */ |
| public @CheckForNull String getResolutionPath() { |
| Object value = get(RESOLUTION_PATH); |
| if (value instanceof String) { |
| return (String) value; |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Sets the {@link #RESOLUTION_PATH_INFO} property to |
| * <code>resolutionPathInfo</code> if not <code>null</code>. |
| * @param resolutionPathInfo The resolution path info |
| */ |
| public void setResolutionPathInfo(String resolutionPathInfo) { |
| if (resolutionPathInfo != null) { |
| put(RESOLUTION_PATH_INFO, resolutionPathInfo); |
| } |
| } |
| |
| /** |
| * Returns the {@link #RESOLUTION_PATH_INFO} property if not |
| * <code>null</code> and a <code>String</code> instance. Otherwise |
| * <code>null</code> is returned. |
| * @return The resolution path info |
| */ |
| public @CheckForNull String getResolutionPathInfo() { |
| Object value = get(RESOLUTION_PATH_INFO); |
| if (value instanceof String) { |
| return (String) value; |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Sets the {@link #PARAMETER_MAP} property to |
| * <code>parameterMap</code> if not <code>null</code>. |
| * @param parameterMap The parameter map |
| */ |
| public void setParameterMap(Map<String, String> parameterMap) { |
| if (parameterMap != null) { |
| if (parameterMap.isEmpty()) { |
| put(PARAMETER_MAP, Collections.emptyMap()); |
| } else { |
| put(PARAMETER_MAP, new LinkedHashMap<String, String>(parameterMap)); |
| } |
| } |
| } |
| |
| /** |
| * Returns the {@link #PARAMETER_MAP} property if not |
| * <code>null</code> and a <code>Map</code> instance. Otherwise |
| * <code>null</code> is returned. |
| * @return The parameter map |
| */ |
| @SuppressWarnings("unchecked") |
| public @CheckForNull Map<String, String> getParameterMap() { |
| Object value = get(PARAMETER_MAP); |
| if (value instanceof Map) { |
| return (Map<String, String>) value; |
| } |
| |
| return null; |
| } |
| |
| |
| /** |
| * Make this object read-only. All method calls trying to modify this object |
| * result in an exception! |
| * @since 2.3 (Sling API Bundle 2.4.0) |
| */ |
| public void lock() { |
| this.isReadOnly = true; |
| } |
| |
| /** |
| * Check if this object is read only and if so throw an unsupported operation exception. |
| */ |
| private void checkReadOnly() { |
| if ( this.isReadOnly ) { |
| throw new UnsupportedOperationException(getClass().getSimpleName() + " is locked"); |
| } |
| } |
| |
| @Override |
| public void clear() { |
| this.checkReadOnly(); |
| super.clear(); |
| } |
| |
| @Override |
| public Object put(@Nonnull final String key, final Object value) { |
| this.checkReadOnly(); |
| return super.put(key, value); |
| } |
| |
| @Override |
| public void putAll(@Nonnull final Map<? extends String, ? extends Object> m) { |
| this.checkReadOnly(); |
| super.putAll(m); |
| } |
| |
| @Override |
| public Object remove(@Nonnull final Object key) { |
| this.checkReadOnly(); |
| return super.remove(key); |
| } |
| |
| protected void internalPut(@Nonnull String key, Object value) { |
| super.put(key, value); |
| } |
| |
| @Override |
| public Object clone() { |
| ResourceMetadata result = (ResourceMetadata) super.clone(); |
| result.lockedEntrySet = null; |
| result.lockedKeySet = null; |
| result.lockedValues = null; |
| return result; |
| } |
| |
| // volatile for correct double-checked locking in getLockedData() |
| private transient volatile Set<Map.Entry<String, Object>> lockedEntrySet; |
| private transient Set<String> lockedKeySet; |
| private transient Collection<Object> lockedValues; |
| |
| private void getLockedData() { |
| if(isReadOnly && lockedEntrySet == null) { |
| synchronized (this) { |
| if(isReadOnly && lockedEntrySet == null) { |
| lockedEntrySet = Collections.unmodifiableSet(super.entrySet()); |
| lockedKeySet = Collections.unmodifiableSet(super.keySet()); |
| lockedValues = Collections.unmodifiableCollection(super.values()); |
| } |
| } |
| } |
| } |
| |
| @Override |
| public @Nonnull Set<Map.Entry<String, Object>> entrySet() { |
| getLockedData(); |
| return lockedEntrySet != null ? lockedEntrySet : super.entrySet(); |
| } |
| |
| @Override |
| public @Nonnull Set<String> keySet() { |
| getLockedData(); |
| return lockedKeySet != null ? lockedKeySet : super.keySet(); |
| } |
| |
| @Override |
| public @Nonnull Collection<Object> values() { |
| getLockedData(); |
| return lockedValues != null ? lockedValues : super.values(); |
| } |
| } |