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}