blob: 541b600e86744c21ac0b7a183e5b765377eadd8c [file] [log] [blame]
/*
* 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.jackrabbit.usermanager.impl.resource;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.jcr.Property;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.jcr.Value;
import javax.jcr.ValueFormatException;
import org.apache.jackrabbit.api.security.user.Authorizable;
import org.apache.jackrabbit.api.security.user.Group;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.jackrabbit.usermanager.resource.SystemUserManagerPaths;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* ValueMap implementation for Authorizable Resources
*/
public class AuthorizableValueMap implements ValueMap {
private static final String DECLARED_MEMBERS_KEY = "declaredMembers";
private static final String MEMBERS_KEY = "members";
private static final String DECLARED_MEMBER_OF_KEY = "declaredMemberOf";
private static final String MEMBER_OF_KEY = "memberOf";
private static final String PATH_KEY = "path";
private static final Logger LOG = LoggerFactory.getLogger(AuthorizableValueMap.class);
private boolean fullyRead;
private final Map<String, Object> cache;
private Authorizable authorizable;
private final SystemUserManagerPaths systemUserManagerPaths;
public AuthorizableValueMap(Authorizable authorizable, SystemUserManagerPaths systemUserManagerPaths) {
this.authorizable = authorizable;
this.cache = new LinkedHashMap<>();
this.fullyRead = false;
this.systemUserManagerPaths = systemUserManagerPaths;
}
@Override
@SuppressWarnings("unchecked")
public <T> T get(String name, Class<T> type) {
if (type == null) {
return (T) get(name);
}
return convertToType(name, type);
}
@Override
@SuppressWarnings("unchecked")
public <T> T get(String name, T defaultValue) {
if (defaultValue == null) {
return (T) get(name);
}
// special handling in case the default value implements one
// of the interface types supported by the convertToType method
Class<T> type = (Class<T>) normalizeClass(defaultValue.getClass());
T value = get(name, type);
if (value == null) {
value = defaultValue;
}
return value;
}
public boolean containsKey(Object key) {
return get(key) != null;
}
public boolean containsValue(Object value) {
readFully();
return cache.containsValue(value);
}
public Set<java.util.Map.Entry<String, Object>> entrySet() {
readFully();
return cache.entrySet();
}
public Object get(Object key) {
Object value = cache.get(key);
if (value == null) {
value = read((String) key);
}
return value;
}
public Set<String> keySet() {
readFully();
return cache.keySet();
}
public int size() {
readFully();
return cache.size();
}
public boolean isEmpty() {
return size() == 0;
}
public Collection<Object> values() {
readFully();
return cache.values();
}
protected Object read(String key) {
// if the item has been completely read, we need not check
// again, as we certainly will not find the key
if (fullyRead) {
return null;
}
try {
if (key.equals(MEMBERS_KEY) && authorizable.isGroup()) {
return getMembers((Group) authorizable, true);
}
if (key.equals(DECLARED_MEMBERS_KEY) && authorizable.isGroup()) {
return getMembers((Group) authorizable, false);
}
if (key.equals(MEMBER_OF_KEY)) {
return getMemberships(true);
}
if (key.equals(DECLARED_MEMBER_OF_KEY)) {
return getMemberships(false);
}
if (key.equals(PATH_KEY)) {
return getPath();
}
if (authorizable.hasProperty(key)) {
final Value[] property = authorizable.getProperty(key);
final Object value = valuesToJavaObject(property);
cache.put(key, value);
return value;
}
} catch (RepositoryException re) {
LOG.error("Could not access authorizable property", re);
}
// property not found or some error accessing it
return null;
}
/**
* Converts a JCR Value to a corresponding Java Object
*
* @param value the JCR Value to convert
* @return the Java Object
* @throws RepositoryException if the value cannot be converted
*/
public static Object toJavaObject(Value value) throws RepositoryException {
switch (value.getType()) {
case PropertyType.DECIMAL:
return value.getDecimal();
case PropertyType.BINARY:
return new LazyInputStream(value);
case PropertyType.BOOLEAN:
return value.getBoolean();
case PropertyType.DATE:
return value.getDate();
case PropertyType.DOUBLE:
return value.getDouble();
case PropertyType.LONG:
return value.getLong();
case PropertyType.NAME: // fall through
case PropertyType.PATH: // fall through
case PropertyType.REFERENCE: // fall through
case PropertyType.STRING: // fall through
case PropertyType.UNDEFINED: // not actually expected
default: // not actually expected
return value.getString();
}
}
protected Object valuesToJavaObject(Value[] values)
throws RepositoryException {
if (values == null) {
return null;
} else if (values.length == 1) {
return toJavaObject(values[0]);
} else {
Object[] valuesObjs = new Object[values.length];
for (int i = 0; i < values.length; i++) {
valuesObjs[i] = toJavaObject(values[i]);
}
return valuesObjs;
}
}
protected void readFully() {
if (!fullyRead) {
try {
if (authorizable.isGroup()) {
cache.put(MEMBERS_KEY, getMembers((Group) authorizable, true));
cache.put(DECLARED_MEMBERS_KEY, getMembers((Group) authorizable, false));
}
cache.put(MEMBER_OF_KEY, getMemberships(true));
cache.put(DECLARED_MEMBER_OF_KEY, getMemberships(false));
String path = getPath();
if (path != null) {
cache.put(PATH_KEY, path);
}
// only direct property
Iterator<String> pi = authorizable.getPropertyNames();
while (pi.hasNext()) {
String key = pi.next();
if (!cache.containsKey(key)) {
Value[] property = authorizable.getProperty(key);
Object value = valuesToJavaObject(property);
cache.put(key, value);
}
}
fullyRead = true;
} catch (RepositoryException re) {
LOG.error("Could not access certain properties of user {}", authorizable, re);
}
}
}
/**
* Reads the authorizable map completely and returns the string
* representation of the cached properties.
*/
@Override
public String toString() {
readFully();
return cache.toString();
}
// ---------- Unsupported Modification methods
public Object remove(Object arg0) {
throw new UnsupportedOperationException();
}
public void clear() {
throw new UnsupportedOperationException();
}
public Object put(String arg0, Object arg1) {
throw new UnsupportedOperationException();
}
public void putAll(Map<? extends String, ? extends Object> arg0) {
throw new UnsupportedOperationException();
}
// ---------- Implementation helper
@SuppressWarnings("unchecked")
private <T> T convertToType(String name, Class<T> type) {
T result = null;
try {
if (authorizable.hasProperty(name)) {
Value[] values = authorizable.getProperty(name);
if (values == null) {
return null;
}
boolean multiValue = values.length > 1;
boolean array = type.isArray();
if (multiValue) {
if (array) {
result = (T) convertToArray(values,
type.getComponentType());
} else if (values.length > 0) {
result = convertToType(values[0], type);
}
} else {
Value value = values[0];
if (array) {
result = (T) convertToArray(new Value[] { value },
type.getComponentType());
} else {
result = convertToType(value, type);
}
}
}
} catch (ValueFormatException vfe) {
LOG.info(String.format("convertToType: Cannot convert value of %s to %s", name, type), vfe);
} catch (RepositoryException re) {
LOG.info(String.format("convertToType: Cannot get value of %s", name), re);
}
// fall back to nothing
return result;
}
private <T> T[] convertToArray(Value[] jcrValues, Class<T> type)
throws RepositoryException {
List<T> values = new ArrayList<>();
for (int i = 0; i < jcrValues.length; i++) {
T value = convertToType(jcrValues[i], type);
if (value != null) {
values.add(value);
}
}
@SuppressWarnings("unchecked")
T[] result = (T[]) Array.newInstance(type, values.size());
return values.toArray(result);
}
@SuppressWarnings("unchecked")
private <T> T convertToType(Value jcrValue, Class<T> type)
throws RepositoryException {
if (String.class == type) {
return (T) jcrValue.getString();
} else if (Byte.class == type) {
return (T) Byte.valueOf((byte) jcrValue.getLong());
} else if (Short.class == type) {
return (T) Short.valueOf((short) jcrValue.getLong());
} else if (Integer.class == type) {
return (T) Integer.valueOf((int) jcrValue.getLong());
} else if (Long.class == type) {
return (T) Long.valueOf(jcrValue.getLong());
} else if (Float.class == type) {
return (T) Float.valueOf((float)jcrValue.getDouble());
} else if (Double.class == type) {
return (T) Double.valueOf(jcrValue.getDouble());
} else if (Boolean.class == type) {
return (T) Boolean.valueOf(jcrValue.getBoolean());
} else if (Date.class == type) {
return (T) jcrValue.getDate().getTime();
} else if (Calendar.class == type) {
return (T) jcrValue.getDate();
} else if (Value.class == type) {
return (T) jcrValue;
}
// fallback in case of unsupported type
return null;
}
private Class<?> normalizeClass(Class<?> type) {
if (Calendar.class.isAssignableFrom(type)) {
type = Calendar.class;
} else if (Date.class.isAssignableFrom(type)) {
type = Date.class;
} else if (Value.class.isAssignableFrom(type)) {
type = Value.class;
} else if (Property.class.isAssignableFrom(type)) {
type = Property.class;
}
return type;
}
private String[] getMembers(Group group, boolean includeAll) throws RepositoryException {
List<String> results = new ArrayList<>();
for (Iterator<Authorizable> it = includeAll ? group.getMembers() : group.getDeclaredMembers();
it.hasNext();) {
Authorizable auth = it.next();
if (auth.isGroup()) {
results.add(systemUserManagerPaths.getGroupPrefix() + auth.getID());
} else {
results.add(systemUserManagerPaths.getUserPrefix() + auth.getID());
}
}
return results.toArray(new String[results.size()]);
}
private String[] getMemberships(boolean includeAll) throws RepositoryException {
List<String> results = new ArrayList<>();
for (Iterator<Group> it = includeAll ? authorizable.memberOf() : authorizable.declaredMemberOf();
it.hasNext();) {
Group group = it.next();
results.add(systemUserManagerPaths.getGroupPrefix() + group.getID());
}
return results.toArray(new String[results.size()]);
}
private String getPath() throws RepositoryException {
try {
return authorizable.getPath();
} catch (UnsupportedRepositoryOperationException e) {
LOG.debug("Could not retrieve path of authorizable {}", authorizable, e);
return null;
}
}
public static class LazyInputStream extends InputStream {
/** The JCR Value from which the input stream is requested on demand */
private final Value value;
/** The inputstream created on demand, null if not used */
private InputStream delegatee;
public LazyInputStream(Value value) {
this.value = value;
}
/**
* Closes the input stream if acquired otherwise does nothing.
*/
@Override
public void close() throws IOException {
if (delegatee != null) {
delegatee.close();
}
}
@Override
public int available() throws IOException {
return getStream().available();
}
@Override
public int read() throws IOException {
return getStream().read();
}
@Override
public int read(byte[] b) throws IOException {
return getStream().read(b);
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
return getStream().read(b, off, len);
}
@Override
public long skip(long n) throws IOException {
return getStream().skip(n);
}
@Override
public boolean markSupported() {
try {
return getStream().markSupported();
} catch (IOException ioe) {
// ignore
}
return false;
}
@Override
public synchronized void mark(int readlimit) {
try {
getStream().mark(readlimit);
} catch (IOException ioe) {
// ignore
}
}
@Override
public synchronized void reset() throws IOException {
getStream().reset();
}
/** Actually retrieves the input stream from the underlying JCR Value */
private InputStream getStream() throws IOException {
if (delegatee == null) {
try {
delegatee = value.getBinary().getStream();
} catch (RepositoryException re) {
throw (IOException) new IOException(re.getMessage()).initCause(re);
}
}
return delegatee;
}
}
}