001/**
002 * Copyright (C) 2006-2025 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.input;
017
018import static java.util.Optional.ofNullable;
019
020import java.io.ByteArrayInputStream;
021import java.io.IOException;
022import java.io.InvalidObjectException;
023import java.io.ObjectInputStream;
024import java.io.ObjectStreamException;
025import java.io.Serializable;
026import java.lang.reflect.Method;
027import java.util.function.Consumer;
028
029import javax.json.bind.Jsonb;
030
031import org.talend.sdk.component.api.component.Version;
032import org.talend.sdk.component.api.input.Producer;
033import org.talend.sdk.component.api.input.checkpoint.CheckpointAvailable;
034import org.talend.sdk.component.api.input.checkpoint.CheckpointData;
035import org.talend.sdk.component.api.service.record.RecordBuilderFactory;
036import org.talend.sdk.component.runtime.base.Delegated;
037import org.talend.sdk.component.runtime.base.LifecycleImpl;
038import org.talend.sdk.component.runtime.record.RecordConverters;
039import org.talend.sdk.component.runtime.serialization.ContainerFinder;
040import org.talend.sdk.component.runtime.serialization.EnhancedObjectInputStream;
041import org.talend.sdk.component.runtime.serialization.LightContainer;
042
043import lombok.AllArgsConstructor;
044import lombok.extern.slf4j.Slf4j;
045
046@Slf4j
047public class InputImpl extends LifecycleImpl implements Input, Delegated {
048
049    private transient Method next;
050
051    private transient RecordConverters converters;
052
053    private transient RecordConverters.MappingMetaRegistry registry;
054
055    private transient Jsonb jsonb;
056
057    private transient RecordBuilderFactory recordBuilderFactory;
058
059    private transient Method checkpoint;
060
061    private transient Method shouldCheckpoint;
062
063    private transient Consumer<CheckpointState> checkpointCallback;
064
065    private boolean checkpointEnabled = Boolean.parseBoolean(System.getProperty("talend.checkpoint.enabled", "false"));
066
067    public InputImpl(final String rootName, final String name, final String plugin, final Serializable instance) {
068        super(instance, rootName, name, plugin);
069    }
070
071    protected InputImpl() {
072        // no-op
073    }
074
075    @Override
076    public void start() {
077        super.start();
078        initCheckpointFunctions();
079    }
080
081    @Override
082    public void start(final Consumer<CheckpointState> checkpointCallback) {
083        start();
084        if (checkpointEnabled) {
085            this.checkpointCallback = checkpointCallback;
086        }
087    }
088
089    protected void initCheckpointFunctions() {
090        if (checkpointEnabled) {
091            checkpoint = findMethods(CheckpointData.class).findFirst().orElse(null);
092            shouldCheckpoint = findMethods(CheckpointAvailable.class).findFirst().orElse(null);
093        }
094    }
095
096    @Override
097    public Object next() {
098        if (next == null) {
099            init();
100        }
101        final Object data = readNext();
102        if (data == null) {
103            return null;
104        }
105        // do we need to checkpoint here?
106        if (isCheckpointReady() && checkpointCallback != null) {
107            checkpointCallback.accept(getCheckpoint());
108        }
109        final Class<?> recordClass = data.getClass();
110        if (recordClass.isPrimitive() || String.class == recordClass) {
111            // mainly for tests, can be dropped while build is green
112            return data;
113        }
114        return converters.toRecord(registry, data, this::jsonb, this::recordBuilderFactory);
115    }
116
117    @Override
118    public CheckpointState getCheckpoint() {
119        if (checkpoint != null) {
120            Object state = doInvoke(this.checkpoint);
121            int version = 1;
122            if (ofNullable(state.getClass().getAnnotation(Version.class)).isPresent()) {
123                version = state.getClass().getAnnotation(Version.class).value();
124            }
125            return new CheckpointState(version, state);
126        }
127        return null;
128    }
129
130    @Override
131    public boolean isCheckpointReady() {
132        boolean checked = checkpointEnabled;
133        if (shouldCheckpoint != null) {
134            checked = (Boolean) doInvoke(this.shouldCheckpoint);
135        }
136        return checked;
137    }
138
139    @Override
140    public void stop() {
141        // do we need to checkpoint here?
142        if (checkpointCallback != null) {
143            checkpointCallback.accept(getCheckpoint());
144        }
145        //
146        super.stop();
147    }
148
149    @Override
150    public Object getDelegate() {
151        return delegate;
152    }
153
154    protected Object readNext() {
155        return doInvoke(this.next);
156    }
157
158    protected void init() {
159        next = findMethods(Producer.class).findFirst().get();
160        converters = new RecordConverters();
161        registry = new RecordConverters.MappingMetaRegistry();
162    }
163
164    private Jsonb jsonb() {
165        if (jsonb != null) {
166            return jsonb;
167        }
168        synchronized (this) {
169            if (jsonb == null) {
170                final LightContainer container = ContainerFinder.Instance.get().find(plugin());
171                jsonb = container.findService(Jsonb.class);
172            }
173        }
174        return jsonb;
175    }
176
177    private RecordBuilderFactory recordBuilderFactory() {
178        if (recordBuilderFactory != null) {
179            return recordBuilderFactory;
180        }
181        synchronized (this) {
182            if (recordBuilderFactory == null) {
183                final LightContainer container = ContainerFinder.Instance.get().find(plugin());
184                recordBuilderFactory = container.findService(RecordBuilderFactory.class);
185            }
186        }
187        return recordBuilderFactory;
188    }
189
190    protected Object writeReplace() throws ObjectStreamException {
191        return new SerializationReplacer(plugin(), rootName(), name(), serializeDelegate());
192    }
193
194    @AllArgsConstructor
195    protected static class SerializationReplacer implements Serializable {
196
197        protected String plugin;
198
199        protected String component;
200
201        protected String name;
202
203        protected byte[] value;
204
205        protected Object readResolve() throws ObjectStreamException {
206            try {
207                return new InputImpl(component, name, plugin, loadDelegate());
208            } catch (final IOException | ClassNotFoundException e) {
209                final InvalidObjectException invalidObjectException = new InvalidObjectException(e.getMessage());
210                invalidObjectException.initCause(e);
211                throw invalidObjectException;
212            }
213        }
214
215        protected Serializable loadDelegate() throws IOException, ClassNotFoundException {
216            try (final ObjectInputStream ois = new EnhancedObjectInputStream(new ByteArrayInputStream(value),
217                    ContainerFinder.Instance.get().find(plugin).classloader())) {
218                return Serializable.class.cast(ois.readObject());
219            }
220        }
221    }
222}