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}