    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *     http://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017
    018package org.apache.commons.jxpath.ri.model.dynamic;
    019
    020import java.util.Arrays;
    021import java.util.Map;
    022
    023import org.apache.commons.jxpath.AbstractFactory;
    024import org.apache.commons.jxpath.DynamicPropertyHandler;
    025import org.apache.commons.jxpath.JXPathAbstractFactoryException;
    026import org.apache.commons.jxpath.JXPathContext;
    027import org.apache.commons.jxpath.JXPathInvalidAccessException;
    028import org.apache.commons.jxpath.ri.model.NodePointer;
    029import org.apache.commons.jxpath.ri.model.beans.PropertyPointer;
    030import org.apache.commons.jxpath.util.ValueUtils;
    031
    032/**
    033 * Pointer to a property of an object with dynamic properties.
    034 */
    035public class DynamicPropertyPointer extends PropertyPointer {
    036
    037    private static final long serialVersionUID = -5720585681149150822L;
    038
    039    /**
    040     * Dynamic property handler.
    041     */
    042    private final DynamicPropertyHandler handler;
    043
    044    /** The name of the currently selected property or "*" if none has been selected. */
    045    private String name;
    046
    047    /** The names of all properties, sorted alphabetically. */
    048    private String[] names;
    049
    050    /**
    051     * The property name from {@link #setPropertyName(String)}.
    052     */
    053    private String requiredPropertyName;
    054
    055    /**
    056     * Constructs a new DynamicPropertyPointer.
    057     *
    058     * @param parent  pointer
    059     * @param handler DynamicPropertyHandler
    060     */
    061    public DynamicPropertyPointer(final NodePointer parent, final DynamicPropertyHandler handler) {
    062        super(parent);
    063        this.handler = handler;
    064    }
    065
    066    @Override
    067    public String asPath() {
    068        final StringBuilder buffer = new StringBuilder();
    069        buffer.append(getImmediateParentPointer().asPath());
    070        if (buffer.length() == 0) {
    071            buffer.append("/.");
    072        } else if (buffer.charAt(buffer.length() - 1) == '/') {
    073            buffer.append('.');
    074        }
    075        buffer.append("[@name='");
    076        buffer.append(escape(getPropertyName()));
    077        buffer.append("']");
    078        if (index != WHOLE_COLLECTION && isCollection()) {
    079            buffer.append('[').append(index + 1).append(']');
    080        }
    081        return buffer.toString();
    082    }
    083
    084    @Override
    085    public NodePointer createPath(final JXPathContext context) {
    086        // Ignore the name passed to us, use our own data
    087        Object collection = getBaseValue();
    088        if (collection == null) {
    089            final AbstractFactory factory = getAbstractFactory(context);
    090            final boolean success = factory.createObject(context, this, getBean(), getPropertyName(), 0);
    091            if (!success) {
    092                throw new JXPathAbstractFactoryException("Factory could not create an object for path: " + asPath());
    093            }
    094            collection = getBaseValue();
    095        }
    096        if (index != WHOLE_COLLECTION) {
    097            if (index < 0) {
    098                throw new JXPathInvalidAccessException("Index is less than 1: " + asPath());
    099            }
    100            if (index >= getLength()) {
    101                collection = ValueUtils.expandCollection(collection, index + 1);
    102                handler.setProperty(getBean(), getPropertyName(), collection);
    103            }
    104        }
    105        return this;
    106    }
    107
    108    @Override
    109    public NodePointer createPath(final JXPathContext context, final Object value) {
    110        if (index == WHOLE_COLLECTION) {
    111            handler.setProperty(getBean(), getPropertyName(), value);
    112        } else {
    113            createPath(context);
    114            ValueUtils.setValue(getBaseValue(), index, value);
    115        }
    116        return this;
    117    }
    118
    119    /**
    120     * Returns the value of the property, not an element of the collection represented by the property, if any.
    121     *
    122     * @return Object
    123     */
    124    @Override
    125    public Object getBaseValue() {
    126        return handler.getProperty(getBean(), getPropertyName());
    127    }
    128
    129    /**
    130     * If index == WHOLE_COLLECTION, the value of the property, otherwise the value of the index'th element of the collection represented by the property. If
    131     * the property is not a collection, index should be zero and the value will be the property itself.
    132     *
    133     * @return Object
    134     */
    135    @Override
    136    public Object getImmediateNode() {
    137        Object value;
    138        if (index == WHOLE_COLLECTION) {
    139            value = ValueUtils.getValue(handler.getProperty(getBean(), getPropertyName()));
    140        } else {
    141            value = ValueUtils.getValue(handler.getProperty(getBean(), getPropertyName()), index);
    142        }
    143        return value;
    144    }
    145
    146    /**
    147     * Number of the DP object's properties.
    148     *
    149     * @return int
    150     */
    151    @Override
    152    public int getPropertyCount() {
    153        return getPropertyNames().length;
    154    }
    155
    156    /**
    157     * Index of the currently selected property in the list of all properties sorted alphabetically.
    158     *
    159     * @return int
    160     */
    161    @Override
    162    public int getPropertyIndex() {
    163        if (propertyIndex == UNSPECIFIED_PROPERTY) {
    164            final String[] names = getPropertyNames();
    165            for (int i = 0; i < names.length; i++) {
    166                if (names[i].equals(name)) {
    167                    setPropertyIndex(i);
    168                    break;
    169                }
    170            }
    171        }
    172        return super.getPropertyIndex();
    173    }
    174
    175    /**
    176     * Gets the name of the currently selected property or "*" if none has been selected.
    177     *
    178     * @return String
    179     */
    180    @Override
    181    public String getPropertyName() {
    182        if (name == null) {
    183            final String[] names = getPropertyNames();
    184            name = propertyIndex >= 0 && propertyIndex < names.length ? names[propertyIndex] : "*";
    185        }
    186        return name;
    187    }
    188
    189    /**
    190     * Gets the names of all properties, sorted alphabetically.
    191     *
    192     * @return String[]
    193     */
    194    @Override
    195    public String[] getPropertyNames() {
    196        if (names == null) {
    197            String[] allNames = handler.getPropertyNames(getBean());
    198            names = new String[allNames.length];
    199            System.arraycopy(allNames, 0, names, 0, names.length);
    200            Arrays.sort(names);
    201            if (requiredPropertyName != null) {
    202                final int inx = Arrays.binarySearch(names, requiredPropertyName);
    203                if (inx < 0) {
    204                    allNames = names;
    205                    names = new String[allNames.length + 1];
    206                    names[0] = requiredPropertyName;
    207                    System.arraycopy(allNames, 0, names, 1, allNames.length);
    208                    Arrays.sort(names);
    209                }
    210            }
    211        }
    212        return names;
    213    }
    214
    215    /**
    216     * A dynamic property is always considered actual - all keys are apparently existing with possibly the value of null.
    217     *
    218     * @return boolean
    219     */
    220    @Override
    221    protected boolean isActualProperty() {
    222        return true;
    223    }
    224
    225    /**
    226     * This type of node is auxiliary.
    227     *
    228     * @return true
    229     */
    230    @Override
    231    public boolean isContainer() {
    232        return true;
    233    }
    234
    235    @Override
    236    public void remove() {
    237        if (index == WHOLE_COLLECTION) {
    238            removeKey();
    239        } else if (isCollection()) {
    240            final Object collection = ValueUtils.remove(getBaseValue(), index);
    241            handler.setProperty(getBean(), getPropertyName(), collection);
    242        } else if (index == 0) {
    243            removeKey();
    244        }
    245    }
    246
    247    /**
    248     * Remove the current property.
    249     */
    250    private void removeKey() {
    251        final Object bean = getBean();
    252        if (bean instanceof Map) {
    253            ((Map) bean).remove(getPropertyName());
    254        } else {
    255            handler.setProperty(bean, getPropertyName(), null);
    256        }
    257    }
    258
    259    /**
    260     * Index a property by its index in the list of all properties sorted alphabetically.
    261     *
    262     * @param index to set
    263     */
    264    @Override
    265    public void setPropertyIndex(final int index) {
    266        if (propertyIndex != index) {
    267            super.setPropertyIndex(index);
    268            name = null;
    269        }
    270    }
    271
    272    /**
    273     * Select a property by name. If the supplied name is not one of the object's existing properties, it implicitly adds this name to the object's property
    274     * name list. It does not set the property value though. In order to set the property value, call setValue().
    275     *
    276     * @param propertyName to set
    277     */
    278    @Override
    279    public void setPropertyName(final String propertyName) {
    280        setPropertyIndex(UNSPECIFIED_PROPERTY);
    281        this.name = propertyName;
    282        requiredPropertyName = propertyName;
    283        if (names != null && Arrays.binarySearch(names, propertyName) < 0) {
    284            names = null;
    285        }
    286    }
    287
    288    /**
    289     * If index == WHOLE_COLLECTION, change the value of the property, otherwise change the value of the index'th element of the collection represented by the
    290     * property.
    291     *
    292     * @param value to set
    293     */
    294    @Override
    295    public void setValue(final Object value) {
    296        if (index == WHOLE_COLLECTION) {
    297            handler.setProperty(getBean(), getPropertyName(), value);
    298        } else {
    299            ValueUtils.setValue(handler.getProperty(getBean(), getPropertyName()), index, value);
    300        }
    301    }
    302}