/*
 * The Kuali Financial System, a comprehensive financial management system for higher education.
 *
 * Copyright 2005-2016 The Kuali Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.kuali.kfs.sys.util;

import org.apache.commons.beanutils.PropertyUtilsBean;
import org.kuali.kfs.krad.util.ObjectUtils;

import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.Map;
import java.util.Set;

/**
 * A Map implementation which wraps a Java bean and can return values from that based on property Strings.  Unlike Apache Common's BeanMap, this Map can handle nested properties - though for the sake of that power,
 * the Map makes no effort to know its Set of keys at all - since to figure all of that out, it would need to recurse down and avoid property cycles, etc.  It does not know how many Entries it has.  Furthermore,
 * one cannot put any values into the Map and expect the underlying bean to have their own values changed.  It's really a read-only way to read nested properties from the underlying object via Map semantics.
 */
public class ReflectionMap implements Map<String, Object> {
    protected Object bean;
    protected PropertyUtilsBean propertyUtilsBean;

    public ReflectionMap(Object bean) {
        if (ObjectUtils.isNull(bean)) {
            throw new IllegalArgumentException("This cannot wrap a null object");
        }
        this.bean = bean;
        this.propertyUtilsBean = new PropertyUtilsBean(); // create our own beanUtilsBean to avoid struts injection of KNS classes
    }

    @Override
    public int size() {
        throw new UnsupportedOperationException("Cannot determine possible size of recursive structure");
    }

    @Override
    public boolean isEmpty() {
        return false;
    }

    @Override
    public boolean containsValue(Object value) {
        throw new UnsupportedOperationException("Will not inspect every value within recursive structure");
    }

    @Override
    public boolean containsKey(Object key) {
        final Object value = get(key);
        return !ObjectUtils.isNull(value);
    }

    @Override
    public Object get(Object key) {
        final String keyAsString = (String) key;
        Object value;
        try {
            value = propertyUtilsBean.getProperty(this.bean, keyAsString);
        } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException | IndexOutOfBoundsException ex) {
            // yep - we know we're swallowing the exception here.  However, we know that bean can't be null, so
            // to fit within regular map semantics, we're just going to return null for the missing key
            return null;
        }
        return value;
    }

    @Override
    public Object put(String key, Object value) {
        throw new UnsupportedOperationException("ReflectionMap is read-only");
    }

    @Override
    public Object remove(Object key) {
        throw new UnsupportedOperationException("ReflectionMap is read-only");
    }

    @Override
    public void putAll(Map m) {
        throw new UnsupportedOperationException("ReflectionMap is read-only");
    }

    @Override
    public void clear() {
        throw new UnsupportedOperationException("ReflectionMap is read-only");
    }

    @Override
    public Set<String> keySet() {
        throw new UnsupportedOperationException("Cannot determine all keys within recursive structure");
    }

    @Override
    public Collection<Object> values() {
        throw new UnsupportedOperationException("Cannot determine all values within recursive structure");
    }

    @Override
    public Set<java.util.Map.Entry<String, Object>> entrySet() {
        throw new UnsupportedOperationException("Cannot determine all entries within recursive structure");
    }
}
