001/** 002 * Copyright (C) 2006-2024 Talend Inc. - www.talend.com 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.talend.sdk.component.runtime.manager.service; 017 018import static java.util.stream.Collectors.toList; 019 020import java.io.ObjectStreamException; 021import java.io.Serializable; 022import java.util.ArrayList; 023import java.util.Collection; 024import java.util.List; 025import java.util.Optional; 026import java.util.stream.Stream; 027 028import org.talend.sdk.component.api.record.Record; 029import org.talend.sdk.component.api.record.RecordPointer; 030import org.talend.sdk.component.api.record.RecordPointerFactory; 031import org.talend.sdk.component.api.record.Schema; 032import org.talend.sdk.component.runtime.serialization.SerializableService; 033 034import lombok.RequiredArgsConstructor; 035 036@RequiredArgsConstructor 037public class RecordPointerFactoryImpl implements RecordPointerFactory, Serializable { 038 039 private final String plugin; 040 041 @Override 042 public RecordPointer apply(final String pointer) { 043 return new RecordPointerImpl(pointer); 044 } 045 046 Object writeReplace() throws ObjectStreamException { 047 return new SerializableService(plugin, RecordPointerFactory.class.getName()); 048 } 049 050 private static class RecordPointerImpl implements RecordPointer, Serializable { 051 052 private final String pointer; 053 054 private final List<String> tokens; 055 056 private RecordPointerImpl(final String pointer) { 057 if (pointer == null) { 058 throw new NullPointerException("pointer must not be null"); 059 } 060 if (!pointer.equals("") && !pointer.startsWith("/")) { 061 throw new IllegalArgumentException("A non-empty pointer string must begin with a '/'"); 062 } 063 064 this.pointer = pointer; 065 this.tokens = Stream.of(pointer.split("/", -1)).map(s -> { 066 if (s == null || s.isEmpty()) { 067 return s; 068 } 069 return s.replace("~1", "/").replace("~0", "~"); 070 }).collect(toList()); 071 } 072 073 @Override 074 public <T> T getValue(final Record target, final Class<T> type) { 075 if (target == null) { 076 throw new NullPointerException("target must not be null"); 077 } 078 if (pointer.equals("") || pointer.equals("/")) { 079 return type.cast(target); 080 } 081 082 Object current = target; 083 final int lastIdx = tokens.size() - 1; 084 for (int i = 1; i < tokens.size(); i++) { 085 current = getValue(current, tokens.get(i), i, lastIdx); 086 } 087 return type.cast(current); 088 } 089 090 public <T> T getEntry(final Record target, final Class<T> type) { 091 if (target == null) { 092 throw new NullPointerException("target must not be null"); 093 } 094 if (pointer.equals("") || pointer.equals("/")) { 095 return type.cast(target); 096 } 097 098 Object current = target; 099 final int lastIdx = tokens.size() - 1; 100 for (int i = 1; i < tokens.size(); i++) { 101 current = getValue(current, tokens.get(i), i, lastIdx); 102 } 103 return type.cast(current); 104 } 105 106 private Object getValue(final Object value, final String referenceToken, final int currentPosition, 107 final int referencePosition) { 108 if (Record.class.isInstance(value)) { 109 final Record record = Record.class.cast(value); 110 final Object nestedVal = getRecordEntry(referenceToken, record); 111 if (nestedVal != null) { 112 return nestedVal; 113 } 114 throw new IllegalArgumentException( 115 "'" + record + "' contains no value for name '" + referenceToken + "'"); 116 } 117 if (Collection.class.isInstance(value)) { 118 if (referenceToken.startsWith("+") || referenceToken.startsWith("-")) { 119 throw new IllegalArgumentException( 120 "An array index must not start with '" + referenceToken.charAt(0) + "'"); 121 } 122 if (referenceToken.startsWith("0") && referenceToken.length() > 1) { 123 throw new IllegalArgumentException("An array index must not start with a leading '0'"); 124 } 125 126 final Collection<?> array = Collection.class.cast(value); 127 try { 128 final int arrayIndex = Integer.parseInt(referenceToken); 129 if (arrayIndex >= array.size()) { 130 throw new IllegalArgumentException( 131 "'" + array + "' contains no element for index " + arrayIndex); 132 } 133 return List.class.isInstance(array) ? List.class.cast(array).get(arrayIndex) 134 : new ArrayList<>(array).get(arrayIndex); 135 } catch (final NumberFormatException e) { 136 throw new IllegalArgumentException("'" + referenceToken + "' is no valid array index", e); 137 } 138 } 139 if (currentPosition != referencePosition) { 140 return value; 141 } 142 throw new IllegalArgumentException("'" + value + "' contains no element for '" + referenceToken + "'"); 143 } 144 145 private Object getRecordEntry(final Schema.Entry entry, final Record record) { 146 switch (entry.getType()) { 147 case STRING: 148 return record.getString(entry.getName()); 149 case INT: 150 return record.getInt(entry.getName()); 151 case LONG: 152 return record.getLong(entry.getName()); 153 case FLOAT: 154 return record.getFloat(entry.getName()); 155 case DOUBLE: 156 return record.getDouble(entry.getName()); 157 case BOOLEAN: 158 return record.getBoolean(entry.getName()); 159 case BYTES: 160 return record.getBytes(entry.getName()); 161 case DATETIME: 162 return record.getDateTime(entry.getName()); 163 case DECIMAL: 164 return record.getDecimal(entry.getName()); 165 case RECORD: 166 return record.getRecord(entry.getName()); 167 case ARRAY: 168 return record.getArray(Object.class, entry.getName()); 169 default: 170 throw new IllegalArgumentException("Unsupported entry type for: " + entry); 171 } 172 } 173 174 private Object getRecordEntry(final String referenceToken, final Record record) { 175 return getEntry(referenceToken, record) 176 .map(entry -> getRecordEntry(entry, record)) 177 .orElseGet(() -> record.get(Object.class, referenceToken)); 178 } 179 180 private Optional<Schema.Entry> getEntry(final String referenceToken, final Record record) { 181 return Optional.ofNullable(record.getSchema().getEntry(referenceToken)); 182 } 183 184 @Override 185 public boolean equals(final Object obj) { 186 if (this == obj) { 187 return true; 188 } 189 if (obj == null || getClass() != obj.getClass()) { 190 return false; 191 } 192 return pointer.equals(RecordPointerImpl.class.cast(obj).pointer); 193 } 194 195 @Override 196 public int hashCode() { 197 return pointer.hashCode(); 198 } 199 } 200}