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}