001/*
002 * ModeShape (http://www.modeshape.org)
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *       http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.modeshape.jdbc.rest;
017
018import static org.modeshape.jdbc.rest.JSONHelper.valueFrom;
019import static org.modeshape.jdbc.rest.JSONHelper.valuesFrom;
020import java.io.ByteArrayInputStream;
021import java.io.IOException;
022import java.io.InputStream;
023import java.math.BigDecimal;
024import java.time.ZoneId;
025import java.time.ZonedDateTime;
026import java.time.format.DateTimeParseException;
027import java.util.Calendar;
028import java.util.Collections;
029import java.util.HashMap;
030import java.util.List;
031import java.util.Map;
032import javax.jcr.Binary;
033import javax.jcr.PropertyType;
034import javax.jcr.RepositoryException;
035import javax.jcr.Value;
036import javax.jcr.ValueFormatException;
037import javax.jcr.version.OnParentVersionAction;
038import org.codehaus.jettison.json.JSONException;
039import org.codehaus.jettison.json.JSONObject;
040import org.modeshape.common.annotation.Immutable;
041import org.modeshape.common.util.DateTimeUtil;
042import org.modeshape.common.util.HashCode;
043import org.modeshape.jdbc.JdbcI18n;
044
045/**
046 * A {@link javax.jcr.nodetype.PropertyDefinition} implementation for the Modeshape client.
047 */
048@Immutable
049public class PropertyDefinition extends ItemDefinition implements javax.jcr.nodetype.PropertyDefinition {
050    private static final Map<String, Integer> PROPERTY_TYPES_BY_LOWERCASE_NAME;
051
052    private final Id id;
053    private final List<String> queryOps;
054    private final List<String> defaultValues;
055    private final List<String> valueConstraints;
056    private final boolean isFullTextSearchable;
057    private final boolean isQueryOrderable;
058
059    static {
060        Map<String, Integer> types = new HashMap<>();
061        registerType(PropertyType.BINARY, types);
062        registerType(PropertyType.BOOLEAN, types);
063        registerType(PropertyType.DATE, types);
064        registerType(PropertyType.DECIMAL, types);
065        registerType(PropertyType.DOUBLE, types);
066        registerType(PropertyType.LONG, types);
067        registerType(PropertyType.NAME, types);
068        registerType(PropertyType.PATH, types);
069        registerType(PropertyType.REFERENCE, types);
070        registerType(PropertyType.STRING, types);
071        registerType(PropertyType.UNDEFINED, types);
072        registerType(PropertyType.URI, types);
073        registerType(PropertyType.WEAKREFERENCE, types);
074        PROPERTY_TYPES_BY_LOWERCASE_NAME = Collections.unmodifiableMap(types);
075    }
076
077    private static void registerType( int propertyType,
078                                      Map<String, Integer> types ) {
079        String name = PropertyType.nameFromValue(propertyType);
080        types.put(name.toLowerCase(), propertyType);
081    }
082
083    protected PropertyDefinition( String declaringNodeTypeName,
084                                  JSONObject json,
085                                  NodeTypes nodeTypes ) {
086        super(declaringNodeTypeName, json, nodeTypes);
087        String name = valueFrom(json, "jcr:name", "*");
088        boolean isMultiple = valueFrom(json, "jcr:multiple", false);
089        int requiredType = typeValueFrom(json, "jcr:requiredType", PropertyType.UNDEFINED);
090        this.id = new Id(name, isMultiple, requiredType);
091        this.isFullTextSearchable = valueFrom(json, "jcr:isFullTextSearchable", false);
092        this.isQueryOrderable = valueFrom(json, "jcr:isQueryOrderable", false);
093        this.queryOps = valuesFrom(json, "jcr:availableQueryOperators");
094        this.defaultValues = valuesFrom(json, "jcr:defaultValues");
095        this.valueConstraints = valuesFrom(json, "jcr:valueConstraints");
096    }
097
098    protected Id id() {
099        return id;
100    }
101
102    @Override
103    public String getName() {
104        return id.name;
105    }
106
107    @Override
108    public String[] getAvailableQueryOperators() {
109        return queryOps.toArray(new String[queryOps.size()]);
110    }
111
112    @Override
113    public Value[] getDefaultValues() {
114        if (defaultValues.isEmpty()) return new Value[0];
115        int numValues = defaultValues.size();
116        int i = 0;
117        Value[] result = new Value[numValues];
118        for (String value : defaultValues) {
119            result[i++] = new StringValue(value);
120        }
121        return result;
122    }
123
124    @Override
125    public int getRequiredType() {
126        return id.requiredType;
127    }
128
129    @Override
130    public String[] getValueConstraints() {
131        return valueConstraints.toArray(new String[valueConstraints.size()]);
132    }
133
134    @Override
135    public boolean isFullTextSearchable() {
136        return isFullTextSearchable;
137    }
138
139    @Override
140    public boolean isMultiple() {
141        return id.isMultiple;
142    }
143
144    @Override
145    public boolean isQueryOrderable() {
146        return isQueryOrderable;
147    }
148
149    @Override
150    public int hashCode() {
151        return id.hashCode();
152    }
153
154    @Override
155    public boolean equals( Object obj ) {
156        if (obj == this) return true;
157        if (obj instanceof PropertyDefinition) {
158            PropertyDefinition that = (PropertyDefinition)obj;
159            return this.id.equals(that.id);
160        }
161        return false;
162    }
163
164    protected int typeValueFrom( JSONObject json,
165                                 String name,
166                                 int defaultType ) {
167        try {
168            if (!json.has(name)) return defaultType;
169            String typeName = json.getString(name);
170            Integer result = PROPERTY_TYPES_BY_LOWERCASE_NAME.get(typeName.toLowerCase());
171            return result != null ? result : defaultType;
172        } catch (JSONException e) {
173            throw new RuntimeException(e);
174        }
175    }
176
177    @Override
178    public String toString() {
179        StringBuilder sb = new StringBuilder();
180        sb.append(" - ");
181        sb.append(id.name);
182        sb.append(" (");
183        sb.append(PropertyType.nameFromValue(id.requiredType));
184        sb.append(')');
185        if (getDefaultValues().length != 0) {
186            sb.append(" = ");
187            boolean first = true;
188            for (Value defaultValue : getDefaultValues()) {
189                if (defaultValue == null) continue;
190                if (first) first = false;
191                else sb.append(',');
192                sb.append(defaultValue);
193            }
194        }
195        if (isAutoCreated()) sb.append(" autocreated");
196        if (isMandatory()) sb.append(" mandatory");
197        if (!isFullTextSearchable()) sb.append(" nofulltext");
198        if (!isQueryOrderable()) sb.append(" noqueryorder");
199        if (isMultiple()) sb.append(" multiple");
200        if (isProtected()) sb.append(" protected");
201        sb.append(' ').append(OnParentVersionAction.nameFromValue(getOnParentVersion()));
202        if (getAvailableQueryOperators().length != 0) {
203            sb.append(" queryops ");
204            boolean first = true;
205            for (String constraint : getAvailableQueryOperators()) {
206                if (constraint == null) continue;
207                if (first) first = false;
208                else sb.append(',');
209                sb.append('\'');
210                sb.append(constraint);
211                sb.append('\'');
212            }
213        }
214        if (getValueConstraints().length != 0) {
215            sb.append(" < ");
216            boolean first = true;
217            for (String constraint : getValueConstraints()) {
218                if (constraint == null) continue;
219                if (first) first = false;
220                else sb.append(',');
221                sb.append(constraint);
222            }
223        }
224        return sb.toString();
225    }
226
227    protected final class StringValue implements Value {
228
229        protected final String value;
230
231        protected StringValue( String value ) {
232            this.value = value;
233            assert this.value != null;
234        }
235
236        @Override
237        public boolean getBoolean() {
238            return Boolean.parseBoolean(value.trim());
239        }
240
241        @Override
242        public Calendar getDate() throws ValueFormatException {
243            return valueToCalendar(null);
244        }
245
246        private Calendar valueToCalendar(String zoneId) throws ValueFormatException {
247            try {
248                ZonedDateTime zonedDateTime = zoneId == null ?
249                                              DateTimeUtil.jodaParse(value) :
250                                              DateTimeUtil.jodaParse(value).withZoneSameInstant(ZoneId.of(zoneId));
251                Calendar calendar = Calendar.getInstance();
252                calendar.setTimeInMillis(zonedDateTime.toInstant().toEpochMilli());
253                return calendar;
254            } catch (DateTimeParseException e) {
255                String from = PropertyType.nameFromValue(getType());
256                String to = PropertyType.nameFromValue(PropertyType.LONG);
257                throw new ValueFormatException(JdbcI18n.unableToConvertValue.text(value, from, to), e);
258            }
259        }
260
261        public Calendar getDateInUtc() throws ValueFormatException {
262            return valueToCalendar("UTC");
263        }
264
265        @Override
266        public BigDecimal getDecimal() throws ValueFormatException {
267            try {
268                if (getRequiredType() == PropertyType.DATE) {
269                    return new BigDecimal(getDateInUtc().getTime().getTime());
270                }
271                return new BigDecimal(value);
272            } catch (NumberFormatException t) {
273                String from = PropertyType.nameFromValue(getType());
274                String to = PropertyType.nameFromValue(PropertyType.DECIMAL);
275                throw new ValueFormatException(JdbcI18n.unableToConvertValue.text(value, from, to), t);
276            }
277        }
278
279        @Override
280        public double getDouble() throws ValueFormatException {
281            try {
282                if (getRequiredType() == PropertyType.DATE) {
283                    return getDateInUtc().getTime().getTime();
284                }
285                return Double.parseDouble(value);
286            } catch (NumberFormatException t) {
287                String from = PropertyType.nameFromValue(getType());
288                String to = PropertyType.nameFromValue(PropertyType.DOUBLE);
289                throw new ValueFormatException(JdbcI18n.unableToConvertValue.text(value, from, to), t);
290            }
291        }
292
293        @Override
294        public long getLong() throws ValueFormatException {
295            try {
296                if (getRequiredType() == PropertyType.DATE) {
297                    return getDateInUtc().getTime().getTime();
298                }
299                return Long.parseLong(value);
300            } catch (NumberFormatException t) {
301                String from = PropertyType.nameFromValue(getType());
302                String to = PropertyType.nameFromValue(PropertyType.LONG);
303                throw new ValueFormatException(JdbcI18n.unableToConvertValue.text(value, from, to), t);
304            }
305        }
306
307        @Override
308        public InputStream getStream() throws RepositoryException {
309            return getBinary().getStream();
310        }
311
312        @Override
313        public String getString() {
314            return value;
315        }
316
317        @Override
318        public int getType() {
319            int type = getRequiredType();
320            return type == PropertyType.UNDEFINED ? PropertyType.STRING : type;
321        }
322
323        @Override
324        public Binary getBinary() {
325            return new Binary() {
326                private byte[] bytes = value.getBytes();
327
328                @Override
329                public void dispose() {
330                    // do nothing
331                    this.bytes = null;
332                }
333
334                @Override
335                public long getSize() {
336                    return bytes.length;
337                }
338
339                @Override
340                public InputStream getStream() {
341                    return new ByteArrayInputStream(bytes);
342                }
343
344                @Override
345                public int read( byte[] b,
346                                 long position ) throws IOException {
347                    if (getSize() <= position) return -1;
348                    try (InputStream stream = getStream()) {
349                        // Read/skip the next 'position' bytes ...
350                        long skip = position;
351                        while (skip > 0) {
352                            long skipped = stream.skip(skip);
353                            if (skipped <= 0) return -1;
354                            skip -= skipped;
355                        }
356                        return stream.read(b);
357                    }
358                }
359            };
360        }
361
362        @Override
363        public String toString() {
364            return value;
365        }
366    }
367
368    protected static class Id {
369        protected final String name;
370        protected final boolean isMultiple;
371        protected final int requiredType;
372
373        protected Id( String name,
374                      boolean isMultiple,
375                      int requiredType ) {
376            this.name = name;
377            this.isMultiple = isMultiple;
378            this.requiredType = requiredType;
379            assert this.name != null;
380        }
381
382        @Override
383        public int hashCode() {
384            return HashCode.compute(isMultiple, requiredType, name);
385        }
386
387        @Override
388        public boolean equals( Object obj ) {
389            if (obj == this) return true;
390            if (obj instanceof Id) {
391                Id that = (Id)obj;
392                if (this.isMultiple != that.isMultiple) return false;
393                if (this.requiredType != that.requiredType) return false;
394                if (!this.name.equals(that.name)) return false;
395                return true;
396            }
397            return false;
398        }
399
400        @Override
401        public String toString() {
402            return name + "(" + PropertyType.nameFromValue(requiredType) + ")" + (isMultiple ? '*' : '1');
403        }
404    }
405
406}